这是什么东东
这是一个实时有偏差Maya到UE的相机预览插件。
至于为什么写这个,是因为我前段时间沉迷在数学的海洋中,正好在学线代和3D数学方面的东西,正好想写写坐标变换相关的,就整理了以前写过的东西,集合成插件,也许会派上用场吧。
这个我目前还不知道有什么用,如果大佬觉得有用,请告诉我。可能加上一键导入资产和场景信息转换就有用了,不用问了,我就是写过才知道(/doge)。
插件演示
安装步骤
解压压缩包,maya插件随意放,建议目录不要有空格或者中文,点击install_modules来安装,安装不上或者安装后没有显示插件的,都是因为我的文件夹下maya文件夹中没有modules文件夹。
maya中装了需要重开maya,找到mtouecamera的插件,点开图标即可。
UE插件需要放到UE项目文件夹中,UE插件根据UE版本来进行放置,如果是4.26就就需要4.26,装对应版本的插件!!
目前只编译了4.24, 4.25, 4.26三个版本,其余的没有编都用不了。
保证项目目录中有Plugins文件夹,打开UE后找到maya to UECamera放置场景中即可。
找到Maya Camera这一栏,打开Open即可
maya设置好UE的项目目录后点击Start
注意,需要两边都开启才有效果,可以看到两边画面一致即成功了
白嫖看我
因为内容较多字数上万,本文不会涉及太多理论上的知识,更多的是偏思路分享,某些概念会简单介绍,如果要大家不想看这么多分析,只需要嫖最后的关键代码或者是插件,就直接跳到最后吧~如果大家想要深入学习,可以去网上查询相关资料或者和资深大佬进行交流学习。虽然我做了很长时间的功课,元旦三天都在写这篇文章,我尽可能去保证我文章的准确性,但如果哪里有纰漏或者是错误,大佬们请轻喷。
插件解析
这个插件最难的地方就是坐标转换和数据交换,关于数据交换我写了两种方式,一种是基于文件读写的缓存,一种就是Socket。文件缓存的话,把数据存成json,在UE中进行tick文件读取就可以了,因为camera,特别是cineCamera,继承的tick是可以在editor的模式下进行。关于Socket我就不多说,讲起来又可以写一篇文章了,我就觉得UE的GC不太好控制,因为我写socket用了多线程,有时候GC时间间隔很长,造成了资源的浪费,不过我觉得应该是我太菜了。。
第二点就是坐标转换了,物体从Maya到UE的坐标最难的不是位移和缩放,毕竟只需要颠倒一下Y值和Z值就可以了,难就难在旋转值上,接下来我借写这个转换camera插件的思路来进行剖析,我是如何去做这个转换的思路。
经典欧拉角
欧拉角是在空间中描述从一个用于表示某个固定的参考系的、已知的方向,经过一系列基本旋转得到、新的代表另一个参考系的方向的方式。
这个方向可以被想成从一个初始的方向,旋转到其确切位置的方向。如下图中描述,原始的参考系的坐标轴被定义为x,y,z,旋转后的坐标系的坐标轴被定义为X,Y,.在几何和物理中,被旋转的坐标系通常被想象成严格附着在一个刚体上。因此,它被称为一个“本地”坐标系,这也意味着它既代表这个刚体的位置,也代表这个刚体的方向。
欧拉角有两种表达形式,简单来说就是经典欧拉角是两轴旋转,泰特布莱恩角是三轴旋转,以下是其所有的旋转顺序:
Proper Euler angles (z-x-z, x-y-x, y-z-y, z-y-z, x-z-x, y-x-y)
Tait–Bryan angles (x-y-z, y-z-x, z-x-y, x-z-y, z-y-x, y-x-z).
泰特-布莱恩角
泰特-布莱恩角中 z-y'-x''这个序列(内旋) 通常被叫做航海角,因为他们可以用来描述一艘船或者一架飞行棋的方向,或者可以叫做万向角,也可以把这几个旋转叫做 heading(摆头),elevation(俯仰)和bank(倾斜),或者叫做 yaw(偏航),pith(俯仰)和roll(翻滚)
所以实际上大多数情况下我们用的是第二种形态的欧拉角,即泰特-布莱恩角,也就是航向角。
坐标轴
划重点了!
3D软件坐标系的定义一般均为:红色X、绿色Y、蓝色Z
我们平时说的旋转都是指按固定轴向旋转,而实际上欧拉角是按照体轴来进行旋转,即旋转顺序为Yaw(Z轴)-> Pitch(Y轴)-> Roll(X轴)
MAYA中欧拉角的旋转轴:
因为坐标系是右手坐标系的,所以UE的旋转方向是逆时针旋转,且Z轴朝上。
UE中欧拉角的旋转轴:
因为坐标系是左手坐标系的,所以UE的旋转方向是顺时针旋转,且Y轴朝上。
这张图很清晰的说明了UE是一个“孤儿”,反正几乎全部的软件导到UE都需要坐标转换就是了。
欧拉角规范化
我们先在UE中调整rotation的数值,在蓝图中打印出来,但是在UE4里,蓝图中的rotation的三个值依次为roll,pitch,yaw。C++中FRotator里是pitch,yaw,roll。
可以发现实际数值和显示数值不一样,实际是因为数值转换成四元数后被规范化了。
我们再来Maya中用Python代码的形式来简单解释这个过程。
import maya.cmds as cmds
from maya.api import OpenMaya as om2
import math
#获取目标当前旋转值
rot_list = cmds.xform(worldSpace=True, rotation=True, query=True)
#角度转换弧度制
rotation_list = [(math.radians(r)) for r in rot_list]
#order=0即使用zyx的顺序旋转
erot = om2.MEulerRotation(*rotation_list, order=0)
#转换为四元数
quat = erot.asQuaternion()
#转换为规范化的旋转值
normalize_rot = quat.asEulerRotation()
#弧度转换角度制
rotation_list = [math.degrees(rad) for rad in normalize_rot]
maya中API的MQuaternion也就是四元数,在欧拉角转换成四元数的形式会自动
规范欧拉角的数值,但是如果用的是cmds的命令或者pymel就需要自己手动规范化,UE中欧拉角转换成四元数的形式后也会自动规范化。
旋转矩阵
在计算坐标变换时,旋转更方便的表示形式是旋转矩阵(Rotation Matrix)。三维空间的旋转矩阵可以表示成3×3的矩阵,单独绕一个轴旋转θ \thetaθ角度的旋转矩阵为:
如果依次绕x轴、y轴、z轴旋转,该变换的旋转矩阵的python代码为:
import numpy as np
import math
def eulerAngles2rotationMat(theta, format='degree'):
if format is 'degree':
theta = [i * math.pi / 180.0 for i in theta]
x, y, z = theta
R_x = [[1, 0, 0],
[0, math.cos(x), -math.sin(x)],
[0, math.sin(x), math.cos(x)]]
R_y = [[math.cos(y), 0, math.sin(y)],
[0, 1, 0],
[-math.sin(y), 0, math.cos(y)]]
R_z = [[math.cos(z), -math.sin(z), 0],
[math.sin(z), math.cos(z), 0],
[0, 0, 1]]
R = np.dot(R_z, np.dot(R_y, R_x))
# 如果需要看到正确结果需要转置
# return R.transpose()
return R
例如当我们给z值为90度的时候,旋转矩阵的值应该是这样子的:
当我们把参数带入函数运行后可以发现,误差可以几乎忽略不计,基本一致。
注意,我们使用的是列向量,但是numpy点积计算结果是行向量,所以我们需要对矩阵进行转置,否则就是下面的结果。
不过后面我们运算也是行向量的运算,所以在最终代码里,可以不用转置矩阵。
接下来我们对求出来的旋转矩阵转转换成UE的欧拉角,也就是ZXY顺序。
根据网上的资料和推导,可以在python的代码为:
def rotationMatrixToEulerAngles(R):
# R = R.transpose()
sx = math.sqrt(R[0, 0] * R[0, 0] + R[0, 1] * R[0, 1])
singular = sx < 1e-6
if not singular:
y = math.atan2(R[0, 2], R[2, 2])
x = math.asin(-R[1, 2])
z = math.atan2(R[1, 0], R[1, 1])
else:
y = math.atan2(R[2, 0], R[2, 2])
x = math.asin(-R[1, 2])
z = 0
return (x, y, z)
因为X=π/2的时候,会导致万向锁,因为篇幅有限,就不具体展开,所以我们需要对其进行判断。
常规情况下,我们需要判断矩阵是否是正交矩阵,否则就会发生错误,但是在这里,我们知道我们求出的矩阵一定是正交矩阵,所以不需要耗费资源去判断了。
def isRotationMatrix(R):
Rt = np.transpose(R)
shouldBeIdentity = np.dot(Rt, R)
I = np.identity(3, dtype=R.dtype)
n = np.linalg.norm(I - shouldBeIdentity)
return n < 1e-6
以下是maya的画面以及相机的旋转值。
下面我们用这段完整代码来检验是否正确。
from maya.api import OpenMaya as om2
import maya.cmds as cmds
import numpy as np
import math
def eulerAngles2rotationMat(theta, format='degree'):
if format is 'degree':
theta = [i * math.pi / 180.0 for i in theta]
x, y, z = theta
R_x = [[1, 0, 0],
[0, math.cos(x), -math.sin(x)],
[0, math.sin(x), math.cos(x)]]
R_y = [[math.cos(y), 0, math.sin(y)],
[0, 1, 0],
[-math.sin(y), 0, math.cos(y)]]
R_z = [[math.cos(z), -math.sin(z), 0],
[math.sin(z), math.cos(z), 0],
[0, 0, 1]]
R = np.dot(R_z, np.dot(R_y, R_x))
return R#.transpose()
def rotationMatrixToEulerAngles(R):
#R = R.transpose()
sx = math.sqrt(R[0, 0] * R[0, 0] + R[0, 1] * R[0, 1])
singular = sx < 1e-6
if not singular:
y = math.atan2(R[0, 2], R[2, 2])
x = math.asin(-R[1, 2])
z = math.atan2(R[1, 0], R[1, 1])
else:
y = math.atan2(R[2, 0], R[2, 2])
x = math.asin(-R[1, 2])
z = 0
return (x, y, z)
if __name__ == "__main__":
dag = om2.MGlobal.getActiveSelectionList().getDagPath(0)
target_transform = om2.MFnTransform(dag)
quad = target_transform.rotation(om2.MSpace.kWorld, True)
rot = quad.asEulerRotation()
r = eulerAngles2rotationMat(rot, "r")
fr = [math.degrees(rt) for rt in rotationMatrixToEulerAngles(r)]
ue_rot = (fr[2], fr[0], -fr[1]-90)
cmds.warning(str(ue_rot))
以下是我们运行的结果,这里我对Z值进行-Z-90是因为maya是Y轴朝上,而UE是Z轴朝上,所以转换需要给X值顺时针旋转π/2,因此我们从ue导出资产到maya也可以看到默认X轴是-90度,而在这里我们就是+90度,但是因为Maya是右手坐标系,而UE是左手坐标系,我们需要给和Z值一个负数,所以就是-(Z+90) = -Z-90。
这是我们设置相同值的结果,可以看到除去焦距因素,物体的方向是一致的。
不过,我们会发现,欧拉角计算旋转变换时,一般需要转换成旋转矩阵,这时候需要计算很多sin, cos,计算量较大,而且我们只针对了ZXY的旋转,如果要转成其他的旋转值轴方向,代码量一定会增加,有没有更高效的办法呢?
四元数
其实我们可以用更加直观的轴角来进行表示,那么四元数就是我们重要的工具,它能够很方便的刻画刚体绕任意轴的旋转。四元数是一种高阶复数,四元数q表示为:
q=(x,y,z,w)=xi+yj+zk+w" role="presentation">
q=(x,y,z,w)=xi+yj+zk+w
在虚幻中也是以四元数的形式去存储旋转方向。
q=(x,y,z,w)=xi+yj+zk+w" role="presentation"> 如图所示,u为旋转轴,旋转角度为σ,向量v旋转到w处。旋转到σ/2处为k如图所示,u为旋转轴,旋转角度为σ,向量v旋转到w处。旋转到σ/2处
q=(x,y,z,w)=xi+yj+zk+w" role="presentation"> 所以 所以
所以,我们用四元数可以大大提升效率。
这是从UE源码中查到的关于四元数的转换,我用Python把关于欧拉角转换四元数的函数码下来了。
def SinCos(Value):
# Map Value to y in [-pi,pi], x = 2*pi*quotient + remainder.
INV_PI = 0.31830988618
HALF_PI = 1.57079632679
quotient = (INV_PI*0.5)*Value
if (Value >= 0.0):
quotient = (float((int(quotient + 0.5))))
else:
quotient = (float((int(quotient - 0.5))))
y = Value - (2.0 * math.pi) * quotient
# Map y to [-pi/2,pi/2] with sin(y) = sin(Value).
if y > HALF_PI:
y = math.pi - y;
sign = -1.0
elif y < -HALF_PI:
y = -math.pi - y;
sign = -1.0
else:
sign = +1.0
y2 = y * y
# 11-degree minimax approximation
ScalarSin = ( ( ( ( (-2.3889859e-08 * y2 + 2.7525562e-06) * y2 - 0.00019840874 ) * y2 + 0.0083333310 ) * y2 - 0.16666667 ) * y2 + 1.0 ) * y
# 10-degree minimax approximation
p = ( ( ( ( -2.6051615e-07 * y2 + 2.4760495e-05 ) * y2 - 0.0013888378 ) * y2 + 0.041666638 ) * y2 - 0.5 ) * y2 + 1.0
ScalarCos = sign*p
return ScalarSin, ScalarCos
def EulerToQuat(Pitch, Yaw, Roll):
DEG_TO_RAD = math.pi/180;
RADS_DIVIDED_BY_2 = DEG_TO_RAD/2;
PitchNoWinding = math.fmod(Pitch, 360.0)
YawNoWinding = math.fmod(Yaw, 360.0)
RollNoWinding = math.fmod(Roll, 360.0)
SP, CP = SinCos(PitchNoWinding * RADS_DIVIDED_BY_2)
SY, CY = SinCos(YawNoWinding * RADS_DIVIDED_BY_2)
SR, CR = SinCos(RollNoWinding * RADS_DIVIDED_BY_2)
x = CR*SP*SY - SR*CP*CY
y = -CR*SP*CY - SR*CP*SY
z = CR*CP*SY - SR*SP*CY
w = CR*CP*CY + SR*SP*SY
return (x,y,z,w)
可以看到在xyz值相同时,结果是一样的。
当然我们不会在python中使用这个,因为在UEC++中,FRotator有一个Quaternion的方法可以转换成四元数。
总结一下,比较容易的方法就是我们根据物体的旋转值求出旋转矩阵,然后转换成UE的欧拉角,轴向角度校正后再把得出的值传到C++中,用Quaternion转换成四元数,存储在FTransform里面就可以实现旋转轴的转换,整体看下来其实并没有什么难度,大佬们也不要被自己吓到了,多试试就出来了hh。
我就是不想写这么多看不懂的怎么办?
本来我想去根据FTransform拿到旋转矩阵或者四元数,将其转换成适用于UE的四元数,这样直接赋值过去效率会更高,但是咱不是搞研究的,KPI才是重点,所以后来我在OpenMaya中找到了一个傻瓜方法,就是拿到物体的四元数,转换成欧拉角后,再对其进行reorder,转换成ZXY的旋转顺序,再对旋转值进行校正,传到UEC++转成四元数就可以了,真的很快有木有,当然知道原理这样去做是也挺节约时间的。
from maya.api import OpenMaya as om2
import math
#获取目标当前旋转值
rot_list = cmds.xform(worldSpace=True, rotation=True, query=True)
#角度转换弧度制
rotation_list = [(math.radians(r)) for r in rot_list]
#order=0即使用zyx的顺序旋转
erot = om2.MEulerRotation(*rotation_list, order=0)
#转换为四元数
quat = erot.asQuaternion()
#转换为规范化的旋转值
normalize_rot = quat.asEulerRotation()
#转换成ZXY的旋转顺序
maya_rot = normalize_rot.reorder(2)
#旋转值校正
maya_rot4 = (-math.degrees(maya_rot.z), math.degrees(maya_rot.x), -math.degrees(maya_rot.y)-90)
附加源码
以下是我扒的旋转矩阵转四元数的源码,有兴趣的话可以研究一下。
inline FQuat::FQuat(const FMatrix& M)
{
// If Matrix is NULL, return Identity quaternion. If any of them is 0, you won't be able to construct rotation
// if you have two plane at least, we can reconstruct the frame using cross product, but that's a bit expensive op to do here
// for now, if you convert to matrix from 0 scale and convert back, you'll lose rotation. Don't do that.
if (M.GetScaledAxis(EAxis::X).IsNearlyZero() || M.GetScaledAxis(EAxis::Y).IsNearlyZero() || M.GetScaledAxis(EAxis::Z).IsNearlyZero())
{
*this = FQuat::Identity;
return;
}
#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST)
// Make sure the Rotation part of the Matrix is unit length.
// Changed to this (same as RemoveScaling) from RotDeterminant as using two different ways of checking unit length matrix caused inconsistency.
if (!ensure((FMath::Abs(1.f - M.GetScaledAxis(EAxis::X).SizeSquared()) <= KINDA_SMALL_NUMBER) && (FMath::Abs(1.f - M.GetScaledAxis(EAxis::Y).SizeSquared()) <= KINDA_SMALL_NUMBER) && (FMath::Abs(1.f - M.GetScaledAxis(EAxis::Z).SizeSquared()) <= KINDA_SMALL_NUMBER)))
{
*this = FQuat::Identity;
return;
}
#endif
//const MeReal *const t = (MeReal *) tm;
float s;
// Check diagonal (trace)
const float tr = M.M[0][0] + M.M[1][1] + M.M[2][2];
if (tr > 0.0f)
{
float InvS = FMath::InvSqrt(tr + 1.f);
this->W = 0.5f * (1.f / InvS);
s = 0.5f * InvS;
this->X = (M.M[1][2] - M.M[2][1]) * s;
this->Y = (M.M[2][0] - M.M[0][2]) * s;
this->Z = (M.M[0][1] - M.M[1][0]) * s;
}
else
{
// diagonal is negative
int32 i = 0;
if (M.M[1][1] > M.M[0][0])
i = 1;
if (M.M[2][2] > M.M[i][i])
i = 2;
static const int32 nxt[3] = { 1, 2, 0 };
const int32 j = nxt[i];
const int32 k = nxt[j];
s = M.M[i][i] - M.M[j][j] - M.M[k][k] + 1.0f;
float InvS = FMath::InvSqrt(s);
float qt[4];
qt[i] = 0.5f * (1.f / InvS);
s = 0.5f * InvS;
qt[3] = (M.M[j][k] - M.M[k][j]) * s;
qt[j] = (M.M[i][j] + M.M[j][i]) * s;
qt[k] = (M.M[i][k] + M.M[k][i]) * s;
this->X = qt[0];
this->Y = qt[1];
this->Z = qt[2];
this->W = qt[3];
DiagnosticCheckNaN();
}
}
终于到了白嫖插件的时间了。
/ 白嫖方法 /
插件应该怎么白嫖呢?
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.