做游戏资产管线的朋友,第一课多半不是建模,也不是引擎操作,而是被坐标系折腾到怀疑人生——尤其是你的生产管线是Maya,终端是UE的时候。我在Maya里认认真真摆好的一套场景,满怀期待导进UE,结果模型横七竖八,要么躺在地上,要么直接脸朝北方,同事看了一眼还问是不是故意做了个“后现代抽象关卡设计”。排查一圈才发现,问题根子不在建模,而在Maya和UE的坐标系定义压根不是一回事。
这篇内容就是把Maya到UE坐标系转换的原理、数学本质、三种实操方案、脚本工具和几十次实测下来踩过的坑全部摊开讲。不管你是刚入行的场景美术,还是要做数据交互管线的技术美术,看完基本都能把坐标转换这件事捋顺,至少不会再被“Y轴向上还是Z轴向上”这种问题半夜叫醒。
1. 项目背景:为什么Maya和UE的坐标系不一样
1.1 一个让我“翻车到天亮”的真实案例
去年做一个虚拟制片预演项目,制片那边给的原始资产都在Maya里,从道具到摄像机路径全部是按Maya习惯摆放的。我在Maya里打开一切正常,镜头调度、角色站位、空间关系全都没问题。结果把同一个FBX导入UE之后,整个场景像是被推了一把侧翻的桌子——所有道具贴地横飞,路灯杆变成了路灯“横杆”,连地面都立起来了。
我当时第一反应是“文件导错了”,反复导了三四遍还是老样子。后来一个做了八年引擎管线的老哥路过,瞟了一眼说:“你把Y-up的东西直接扔给Z-up的引擎,它不倒才怪。” 那一刻我才意识到,坐标轴选型这东西,看起来只是三个字母的差异,一旦在项目里踩中,代价就是几个通宵。
1.2 Maya(右手+Y-Up)与UE(左手+Z-Up)的坐标系差异
先把定义掰清楚。三维软件里的“右手坐标系”和“左手坐标系”,通俗理解就是:你伸出右手的拇指、食指和中指,让它们两两垂直,拇指指向X方向、食指指向Y方向、中指指向Z方向,这套方向定义就是右手系;换成左手就是左手系。Maya采用的正是右手坐标系,并且把Y轴作为垂直向上的轴,X轴朝右,Z轴朝屏幕外。
UE则不一样。UE用的是左手坐标系,且Z轴向上,X轴朝前(Forward),Y轴朝右(Right)。也就是说,同一个模型,在Maya里是“站”在Y轴上的,到UE里必须“改站”到Z轴上;除此之外,因为手性不同,从一个坐标系的Z轴正方向映射到另一个坐标系时,其中一个轴的方向必须翻转,不然会变成镜像。
把两者摆在一起看,Maya和UE的核心差异就两条:
| 项目 | Maya | UE |
|---|---|---|
| 手性 | 右手坐标系 | 左手坐标系 |
| 向上轴 | Y轴 | Z轴 |
| 前向轴 | Z轴(朝屏幕外) | X轴 |
| 默认单位 | 厘米(一般) | 厘米(默认) |
所以说Maya到UE的坐标系转换,本质上不是“点一下旋转”这么简单。它涉及向上轴的重定向、前向轴的重映射,以及可能出现的左右手翻转。这三个问题单独处理都不难,但放在一个带有骨骼动画、法线、光照和缩放的资产上时,就容易连锁翻车。
1.3 坐标系差异会导致哪些问题
如果是纯网格模型,坐标系搞错最多就是模型躺倒,转90度就能救回来。但一旦资产包含下面几类信息,问题就远不只是一个旋转值的事:
- 模型朝向:Maya里Z轴正向朝观察者,UE里X轴正向朝前进方向。导向不当,角色导进去会侧着走、倒着走,甚至“面朝地板滑行”。
- 动画数据:骨骼的每个关键帧都记录了旋转和位移,坐标系不同意味着所有关键帧都需要被重映射。如果只把根节点旋转一下而不管骨骼子节点,动画大概率会扭曲。
- 法线方向:法线是依赖模型空间的。轴翻转之后如果处理不好,光照会出现“半边脸黑”的诡异效果。
- 摄像机路径:影视预演项目里尤其明显,Maya里的摄像机飞行路径一旦坐标系映射错,导进UE就是一台“穿地摄像机”。
- 数据流对接:比如从Maya里导出的点云、路径点、追踪数据,通过CSV或TXT喂给UE时,坐标字段如果不转换,点位会直接跑到完全错误的位置,排查起来非常痛苦。
这也是为什么我建议每个涉及Maya和UE双端的团队,都要把坐标系转换做成标准操作流程,而不是每次靠手动旋转“碰运气”。
2. 转换核心思路:轴映射与旋转矩阵
2.1 从Y-Up到Z-Up的本质:绕X轴旋转90°
先说一个很容易被忽略的事实:从Maya坐标系到UE坐标系,并不是一个“各轴分别缩放映射”的线性变换,而是一个绕X轴旋转90°的纯旋转变换。
试想一个点,在Maya里的坐标是(0, 1, 0),也就是Y轴正半轴上的点。它对应到UE里应该是(0, 0, 1),也就是Z轴正半轴上的点。把(0,1,0)变成(0,0,1),可以绕X轴旋转+90°。
再看一个点,Maya里的(0, 0, 1)(Z轴正方向),绕X轴旋转+90°后变为(0, -1, 0),即UE的Y轴负方向。于是可以验证,在绕X轴旋转+90°的变换下:
- X轴保持不变;
- Y轴向Z轴方向转;
- Z轴向Y轴负方向转。
换算成公式就是:在Maya中取任意点(x, y, z),映射到UE坐标系后为(x, -z, y)。这个关系的行列式是+1,意味着这个变换是纯旋转,没有产生镜像,不需要额外做左右手翻转。
2.2 位置、旋转、缩放的映射关系
上面这个公式适用于位置坐标,那旋转和缩放呢?
缩放是最好办的,坐标系的轴手性和缩放没有关系,如果Maya和UE都用厘米作为单位,缩放在转换过程中保持不变。
旋转信息就复杂一些。如果你只是把模型整体绕X轴转90°,那么模型自身的旋转角度的意义也完全变了。四元数或者欧拉角都需要经过“基向量重映射”才会得到UE语义下的正确朝向。最简单的做法是提取Maya中模型的世界矩阵W,用上面那个旋转矩阵M去做相似变换,也就是:
W_UE = M × W_Maya × M⁻¹
其中M是2.1里那个轴映射矩阵。这个式子做的事本质上就是把Maya世界的旋转矩阵整体搬到UE世界坐标系里,同时保留旋转本身的实际物理方向。对于大部分静态模型和带骨骼的网格,用这条变换逻辑去处理旋转值是可靠的。
2.3 选型:三种主流方案对比
知道数学原理,接下来就是工程落地。我常用三种方案,它们分别适用于不同场景:
| 方案 | 手段 | 适用场景 | 优势 | 注意点 |
|---|---|---|---|---|
| A | Maya FBX导出时启用轴转换 | 绝大多数资产导入 | 无损、自动、可批量 | 需要确认FBX Explorer设置 |
| B | Maya内脚本旋转根组 | 静态场景、临时调整 | 快速、直观 | 绑定角色慎用 |
| C | UE端调整导入/运行时Transform | 资产已经导入不打算重导 | 不用回到DCC | 要在每次导入时手动确认 |
下面三个章节就分别把这三条路线展开,每一步都给可以直接抄的参数和脚本。
3. 方案A:通过Maya FBX导出设置完成转换(最推荐)
3.1 导出前的场景检查
在动FBX导出设置之前,先把Maya里的场景做一次“体检”,能省下后面很多事。首先确认场景单位:Maya的默认工作单位如果设成了厘米,和UE默认单位一致,那你就不需要担心缩放;如果你习惯用米或者英寸,请先把它切回厘米,再检查模型本身的比例。
其次是确认Maya场景里的实际轴向。你可以创建一个Locator,在世界原点看它的局部坐标轴,确认Y轴向上。如果场景里面有些模型是建模阶段用Z轴向上的模板生成的,那单独看这个物体的局部坐标轴会和别的物体不一致,这种情况要先统一物体轴向,否则导出的FBX也会带着混乱的局部空间。
最后,如果是带骨骼的角色,建议先把根骨骼的“设置初始姿态”做个快照,防止后面调整坐标时骨骼的初始姿态被污染。
3.2 FBX Axis Conversion参数设置
在Maya里导出FBX时,如果没有额外配置轴转换,插件通常会把场景里所有坐标连同“Y-up”的信息一起写进FBX。UE读取FBX时会读取文件里的Up Axis信息,所以理论上即使不手动设置,UE也能识别。但这个机制并不总是按预期工作,尤其是Maya版本比较旧、FBX插件版本不统一时,轴信息容易被写错。
更稳妥的做法,是在Maya的FBX导出设置面板里,找到Axis Conversion选项。菜单路径大致是“File > Export All / Export Selection”后在选项窗口的Advanced Options一节里。不同Maya版本叫法略有差异,但核心就是设置目标Up Axis为Z:
- Source Up Axis:Y(标识Maya场景Y向上)
- Target Up Axis:Z(告诉FBX导出器,最终要转成Z向上)
- 如果有Source Front Axis / Target Front Axis的选项,继续保持默认,不要乱改。
设置完成后,把模型重新导出一份FBX,再导入UE。UE里可以看到模型是直立的,而且前向朝向也大致正确。
3.3 在UE中导入并检查
在UE Content Browser里双击FBX文件,会弹出导入设置面板。如果你的Maya导出设置正确,这里有几点可以直接看到:
- Import Uniform Scale保持1.0;
- Import Translation为(0,0,0);
- Import Rotation为(0,0,0);
- 注意“Mesh → Transform → Rotation”里如果没有额外偏移,说明FBX文件内的坐标轴信息已经被正确读取。
导入后一定要做三件事:把StaticMesh放进关卡看朝向;如果是骨骼网格,播放一遍动画看骨骼是否有异常扭曲;再打一个方向光看模型表面的法线是否正常。这三个检查过了,这条链路就是通的。
4. 方案B:使用Maya Python脚本批量转换坐标
4.1 场景根组旋转脚本
FBX自动转换当然省事,但当你的需求是“把Maya场景里的所有对象直接变成UE坐标,然后再做其他深度处理”时,在Maya里用脚本旋转根组反而更灵活。比如,你想导出一个带点云、哑光物件、路径曲线的原始场景到UE,FBX轴转换不一定能处理所有自定义属性,这时脚本方案就显出优势。
下面这段代码可以在Maya中创建根组、把场景所有可选对象挂进去、绕X轴旋转90°,然后冻结变换。
import pymel.core as pm def rotate_all_to_ue(selection_only=True): # 选择要转换的对象。如果只转换选中项,就取当前选中;否则取场景所有dagObject if selection_only: items = pm.ls(sl=True, dag=True, long=True) if not items: pm.warning("请先选中要转换的节点或组") return else: items = pm.ls(assemblable=True, long=True) # 创建根组 root = pm.group(empty=True, name='UE_coordinate_root') pm.parent(items, root) # 绕X轴旋转正90度,让Y-up变成Z-up pm.xform(root, rotation=(90, 0, 0)) # 冻结变换:把旋转写回所有子对象的本地空间 pm.makeIdentity(root, apply=True, translate=True, rotate=True, scale=True) # 删除成组用的空组,因为变换已经被冻结到子对象 pm.delete(root) print("坐标转换完成:Maya Y-up -> UE Z-up")需要注意,这段代码会修改每个对象本地空间的旋转值。纯粹的场景物件没问题,但如果是绑定了骨骼且带有动画的角色,直接执行会导致动画曲线含义改变,整体动画表现可能会异常。这种时候把模型和动画作为两套数据分开处理会更稳妥。
4.2 逐点坐标转换脚本(适用于CSV/TXT数据)
有时候你手里拿到的不是场景文件,而是一份Maya里导出的坐标列表,比如标记点、麦克风阵列点位、角色路径点。这里只需要套用前面推导的位置映射公式:UE坐标 = (x, -z, y)。
def maya_point_to_ue(point): x, y, z = point return (x, -z, y) # 示例:读取Maya导出的点列表,输出UE可用的点列表 import csv def convert_csv_maya_to_ue(input_path, output_path): with open(input_path, 'r') as fin, open(output_path, 'w', newline='') as fout: reader = csv.reader(fin) writer = csv.writer(fout) header = next(reader) writer.writerow(header + ['X_UE', 'Y_UE', 'Z_UE']) for row in reader: # 假设前三列分别对应Maya的x, y, z x_m, y_m, z_m = float(row[0]), float(row[1]), float(row[2]) xu, yu, zu = maya_point_to_ue((x_m, y_m, z_m)) writer.writerow(row + [xu, yu, zu])这个脚本在两种软件之间搬运数据时特别好用,尤其是当数据不是通过FBX传输,而是走CSV/TXT/JSON这类中间格式时。我经常在动捕棚里用这个脚本把Maya整理好的标记点位转换成UE里的Actor布局,效率比手敲高得多。
4.3 动画与骨骼的注意事项
对带骨骼动画的角色,我的建议是:不要轻易用根组旋转脚本,更不要对骨骼链整体绕X轴转90°之后直接导出动画。原因很简单:动画曲线在骨骼子空间里存储的是相对父骨骼的旋转,根节点旋转后,父骨骼的旋转值变了,但子骨骼的局部旋转本质上已经隐含了旋转偏移,两者一叠加,动画就会变成“拧毛巾”。
正确处理方式分两步走:第一步,在Maya里把模型和控制器整体转到UE坐标(同样用根组旋转),随后导出静止模型;第二步,导出动画时设置FBX导出的轴转换,并把根骨骼的旋转归零后作为基础姿态。这样模型是UE坐标,动画数据也经过正确重映射,两者在UE里才能合得上。这个流程我踩过很多次坑才总结出来。
5. 方案C:在UE端使用蓝图/C++修正
5.1 UE导入FBX时的Transform设置
有些时候资产已经在项目里了,重新回Maya再导一次流程成本太高,这时候直接在UE里修正也可以。
双击FBX打开导入设置时,重点看“Transform”区域。如果导入之后模型躺倒,也就是Y轴朝上的模型被当成Z轴朝上了,一般做法是调整“Import Rotation”,把Pitch设为-90度或者-90的变体。也有一部分依赖Yaw的,比如模型虽然直立但朝向偏了,那就把Yaw改成90、-90、180逐个试。
这里有一个非常重要的经验:不要直接在导入设置里把Import Rotation改成一个非零值就完事,而是要检查模型导入后的本地朝向是否和你资产库的标准朝向一致。如果你把“Pitch -90”写死在导入设置里,后续每个新模型都要保证同样的初始姿态,否则一堆模型进场景后各转各的,统一管理会变成灾难。
5.2 运行时蓝图坐标修正
如果是数据驱动的场景,比如在Maya标定好一串路径点,通过CSV给UE里的物体做运动,在UE端用蓝图做运行时修正会更灵活。做法不复杂:从CSV读入点位之后,把每个点的Maya坐标做一次轴映射,再传给你要控制的Actor。
蓝图里只需把从文件读到的Vector值,经过“MayaToUE”函数转换。转换逻辑就是三个节点:
- X保持不变;
- Y输入取负,然后接到新Vector的Z;
- Z输入接到新Vector的Y。
用蓝图节点描述:从原始向量拿到X、Y、Z,然后MakeVector(XNew = X原始, YNew = -Z原始, ZNew = Y原始)。这个函数小组件可以做成工具蓝图或者放进通用函数库,项目里所有需要从Maya数据流接入的地方都能复用。
5.3 C++方式:FTransform换算
如果你的工具链跑在UE C++层,需要用代码处理外部传入的Maya坐标,核心就是一个FTransform换算。注意UE的FMatrix采用行向量右乘规则,轴映射矩阵要写成前面推导的矩阵的转置形式:
FTransform ConvertMayaToUE(const FTransform& InMayaTransform) { // 映射矩阵:Maya (x, y, z) -> UE (x, -z, y) const FMatrix A2B = FMatrix( FPlane(1, 0, 0, 0), FPlane(0, 0, 1, 0), FPlane(0, -1, 0, 0), FPlane(0, 0, 0, 1)); const FMatrix MayaMatrix = InMayaTransform.ToMatrixWithScale(); const FMatrix UEMatrix = MayaMatrix * A2B; return FTransform(UEMatrix); }这里把Maya的矩阵先取出来,再乘上映射矩阵,就得到了UE坐标语义下的Transform。从C++层面做的好处是可以直接接到数据管线中间件里,比如通过UDP/TCP接收Maya实时动捕数据,在UE端即时转换成引擎坐标,再驱动虚拟角色,整体延迟可控,表现也稳定。
6. 进阶:用CSV/TXT批量转换坐标数据并在UE中导入
6.1 常见需求:Maya标定的点云/路径数据导入UE
做虚拟制片或者建筑可视化项目时,经常遇到一个很实际的需求:Maya里有一张标定好的点位表,可能包含几十甚至上百个标记点,对应的是真实演播厅里的机位或者道具摆放位置,需要用CSV/TXT格式传给UE,在UE里瞬间生成对应的Actor阵列。
如果你直接在UE里导入Maya导出的原始CSV,点位的位置通常会有一半在地底下、一半在天上,因为坐标轴定义不同。这种情况需要把逐点转换做成导入流程自动完成,而不是每次拖入Excel手动加工。
6.2 写一个Python小工具批量转换
我一般会在项目里放一个独立的Python小工具,负责读Maya导出文件、做坐标变换、输出UE能用的列表文件。这个工具不依赖Maya环境,所以美术和TA都能独立使用。
import csv def maya_to_ue_row(row): x_m, y_m, z_m = float(row[0]), float(row[1]), float(row[2]) return (x_m, -z_m, y_m) def convert_file(input_path, output_path): with open(input_path, 'r') as fin: reader = csv.reader(fin) header = next(reader) rows = list(reader) with open(output_path, 'w', newline='') as fout: writer = csv.writer(fout) writer.writerow(header) # 输出同样表头 for row in rows: writer.writerow(maya_to_ue_row(row))这个工具唯一的输出差异是坐标字段已经被替换成UE坐标,不额外增加列,所以下游程序不需要改动字段索引,比较省心。
6.3 在UE编辑器用Python读CSV并生成Actor
UE从4.26开始,编辑器本身支持Python脚本扩展。你可以启用“Python Editor Script Plugin”和“Editor Scripting Utilities”,然后在编辑器的Python控制台里执行脚本,直接读取CSV并在关卡中生成Actor。示例代码如下:
import csv import unreal def spawn_points_from_csv(csv_path): with open(csv_path, 'r') as f: reader = csv.reader(f) next(reader) # 跳过表头 for idx, row in enumerate(reader): x, y, z = maya_to_ue_point((float(row[0]), float(row[1]), float(row[2]))) loc = unreal.Vector(x, y, z) actor = unreal.EditorLevelLibrary.spawn_actor_from_class( unreal.PointLight, loc, unreal.Rotator(0, 0, 0) ) actor.set_actor_label(f"Point_{idx:03d}")用PointLight只是为了方便肉眼验证点位,你也可以换成EmptyActor或者继承自自定义蓝图类的Actor。这个方法非常适合在关卡里快速铺开标定点阵列。
7. 常见问题与排查技巧实录
7.1 模型躺倒/翻转
问题表现:模型在Maya里直立,导入UE后平躺、横躺,或者以任意角度侧躺。
排查思路:先看FBX文件本身的Axis信息。用Maya重新导出一份FBX,并在导出设置里查看Axis Conversion部分,确认Source Up Axis是Y、Target Up Axis是Z。如果设置没问题但依然躺倒,那么大概率是场景里根节点的局部坐标轴本身就不是标准Y-up,比如模型是从老项目里拷贝过来、根骨骼被人为旋转过。这种情况在Maya里选中根节点,打开Channel Box,检查Rotate属性是否非零,若非零则先清零再导出。
7.2 模型前后镜像/左右颠倒
问题表现:模型导入UE后,不是“歪倒”,而是左右翻转或者前后反转,比如把右手模型导成了左手模型。
这种通常是FBX文件的轴向定义与UE的导入设置不匹配导致的。处理方法:检查Maya导出的FBX是否包含Handedness信息。如果你的DCC工具是Blender或者自研导出器,很容易在这里出问题。对这种情况,我通常在UE导入设置里通过旋转变换解决,而不是回Maya重做。如果是前后反转,设置Yaw=180;如果是左右镜像,需要额外在网格层级做镜像处理,这个就涉及网格几何翻转,比旋转复杂,要特别小心。
7.3 动画、法线、灯光异常
问题表现:模型看着方向正确,但播放动画时角色肢体“拧麻花”;或者模型表面光照明暗交替,看起来像被“粗糙刷子”刷过。
动画异常基本都是旋转映射没做完整。不要只转根节点,要确保骨架层级里的每个骨骼关节都经过同一套轴映射。如果你的动画是通过FBX导出,建议先导出一个T-Pose检查骨骼朝向,再导正常动画。法线异常则多半是模型在转换时遭遇过非等比缩放或者导入设置里Normal Import Method选错了,建议把“Import Normals”设为“Calculate Normals”,重新计算一次。
7.4 缩放和单位问题
问题表现:模型大小差10倍、100倍,或者平时用Maya做1米高的杯子,导入UE却变成一颗摩天楼。
先核对两边单位。Maya如果设成米,UE默认厘米,不换算的话一个1米见方的物件进UE就是1厘米大小。同理,Maya里的角色如果是190cm,导出FBX时确认“FBX Units”设置为Automatic,让单位信息写进文件。导入UE时保持Import Uniform Scale=1.0。如果已经导成了1厘米的小玩意,直接改缩放是下策,最好是回Maya把单位改对再重导,避免隐藏的缩放污染后续动画和物理计算。
7.5 特殊:其他DCC导出的FBX也要统一处理
这条往往被忽视。团队里除了Maya,还有人用Blender、Houdini或者C4D。Blender默认也是Y-up右手系(与Maya相近),但有些设置会覆盖;Houdini默认也是Y-up但可配置。因此所有资产入库前都应该有一道“轴向检查”关卡,哪怕只是写一个小工具,在导入GIT仓库前自动检查FBX头部的Up Axis和Front Axis字段。提前拦截问题,比出镜到UE里再返工要划算得多。
我个人在实际生产里最大的体会是:坐标系转换不是一道“加个旋转节点”就结束的工序,它是一条必须写进资产规范的约定。最好在项目启动第一天就确认好“DCC标准坐标是Y-up右手系,引擎标准坐标是Z-up左手系,中间转换责任方是谁、工具是什么”,并沉淀成团队工具链的一部分。只有把轴映射公式、FBX导出选项、脚本转换、UE导入设置、CSV数据流这几层都打通,后续做动画、虚拟制片、多工具联调时才不会再被“哪个轴朝上”这种基础问题拖慢进度。希望这篇内容能帮你少踩几个坑,直接把坐标转换这堵墙拆平。