CFrame 数学运算¶
Time:15 分钟
阅读本文章前需要具备较为深厚的向量和向量数学知识。请刚接触 CFrame 的开发者先从articles/Understanding CFrame|了解 CFrames
一文开始阅读。
CFrame 的分量¶
每个 CFrame 都由 12 个单独的数字组成,我们称其为分量。当需要获取这些数字时,只需调用能够将其作为结果返回的 CFrame:components()
方法即可。
local x, y, z, m11, m12, m13, m21, m22, m23, m31, m32, m33 = cf:components()
当定义 CFrame 时,开发者也可以直接输入这 12 个数字。
local cf = CFrame.new(x, y, z, m11, m12, m13, m21, m22, m23, m31, m32, m33)
12 个数字中的前三个分别为 CFrame 的 x、y 和 z 分量,也就是其中包含的坐标位置。剩余的数字组成了 CFrame 的旋转方位。这些数字看上去可能有些复杂,但当我们对其换个方式稍加整理就会发现,这些数列分别代表的是 rightVector、upVector 和负 lookVector。
local cf = CFrame.new(0, 0, 0)
local x, y, z, m11, m12, m13, m21, m22, m23, m31, m32, m33 = cf:components()
-- m11, m12, m13,
-- m21, m22, m23,
-- m31, m32, m33
local right = Vector3.new(m11, m21, m31) -- 与 cf.rightVector 相同
local up = Vector3.new(m12, m22, m32) -- 与 cf.upVector 相同
local back = Vector3.new(m13, m23, m33) -- 与 -cf.lookVecto 相同
对这些向量进行视觉化后,我们将可以更轻易地体会 CFrame 旋转数值的实际效果。从下图中可以看到这些数字代表的是三个正交向量,它们共同描绘了一个 3D 球体状旋转范围。
CFrame * CFrame¶
CFrame 实际上是采用以下形式的 4x4 矩阵:
也就是说,我们只需将两个 4x4 的矩阵相乘,就能轻松地将两个 CFrame 相乘!
这样我们就能编写出用于将两个 CFrame 相乘的函数!
local function multiplyCFrame(a, b)
local ax, ay, az, a11, a12, a13, a21, a22, a23, a31, a32, a33 = a:components()
local bx, by, bz, b11, b12, b13, b21, b22, b23, b31, b32, b33 = b:components()
local m11 = a11*b11+a12*b21+a13*b31
local m12 = a11*b12+a12*b22+a13*b32
local m13 = a11*b13+a12*b23+a13*b33
local x = a11*bx+a12*by+a13*bz+ax
local m21 = a21*b11+a22*b21+a23*b31
local m22 = a21*b12+a22*b22+a23*b32
local m23 = a21*b13+a22*b23+a23*b33
local y = a21*bx+a22*by+a23*bz+ay
local m31 = a31*b11+a32*b21+a33*b31
local m32 = a31*b12+a32*b22+a33*b32
local m33 = a31*b13+a32*b23+a33*b33
local z = a31*bx+a32*by+a33*bz+az
return CFrame.new(x, y, z, m11, m12, m13, m21, m22, m23, m31, m32, m33)
end
我们还可以通过循环到达同样的结果:
local function multiply4x4(a, b)
local out = {}
for i = 1, 16 do
out[i] = 0
local r = math.floor((i-1)/4)+1
local p = i%4 == 0 and 4 or i%4
for j = 1, 4 do
local ai = (r-1)*4+j
local bi = p+(j-1)*4
out[i] = out[i] + a[ai]*b[bi]
end
end
return out
end
local function multiplyCFrame(a, b)
local ax, ay, az, a11, a12, a13, a21, a22, a23, a31, a32, a33 = a:components()
local bx, by, bz, b11, b12, b13, b21, b22, b23, b31, b32, b33 = b:components()
a = {a11, a12, a13, ax, a21, a22, a23, ay, a31, a32, a33, az, 0, 0, 0, 1}
b = {b11, b12, b13, bx, b21, b22, b23, by, b31, b32, b33, bz, 0, 0, 0, 1}
local m11, m12, m13, x, m21, m22, m23, y, m31, m32, m33, z = unpack(multiply4x4(a, b))
return CFrame.new(x, y, z, m11, m12, m13, m21, m22, m23, m31, m32, m33)
end
最后让我们来进行验证测试
local cf = CFrame.new(1, 2, 3) * CFrame.Angles(math.rad(14), math.rad(72), math.rad(-32));
print(cf*cf)
print(multiplyCFrame(cf, cf))
4.44273901, 3.34623194, 2.4210279, -0.849777162, -0.0723331869, 0.522155881, -0.316965073, 0.861586094, -0.396487743, -0.421203077, -0.502431393, -0.755083263
4.44273901, 3.34623194, 2.4210279, -0.849777162, -0.0723331869, 0.522155941, -0.316965073, 0.861586154, -0.396487743, -0.421203077, -0.502431393, -0.755083263
进行 CFrame 相乘时有一点十分重要:CFrame 的乘数是不能互换的。换而言之,a * b 并不一定等于 b * a。
local cf1 = CFrame.new(1, 2, 3) * CFrame.Angles(math.rad(14), math.rad(72), math.rad(-32))
local cf2 = CFrame.new(0.1, -10, 6) * CFrame.Angles(math.rad(90), math.rad(-28), math.rad(-86))
print(cf1*cf2)
print(cf2*cf1)
5.09500504, -7.92827415, 7.54646206, -0.937961817, 0.220474482, -0.26761657, 0.0239842981, -0.728708208, -0.684404194, -0.345908046, -0.64836365, 0.678212643
0.514770269, -13.618248, 5.1419487, 0.162701935, 0.975506902, -0.148035079, 0.94501543, -0.197204709, -0.260875672, -0.283679247, -0.0974504724, -0.953954697
这项规则也有几种例外情况,其中一种是逆运算,我们稍后再做讨论,而另一种就是我们现在要谈到的身份识别 CFrame。
身份识别 CFrame 如下所示:
local identityCFrame = CFrame.new(0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1)
-- note: CFrame.new(0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1) == CFrame.new()
如果我们自前或自后用 CFrame 乘以身份识别 CFrame,我们只会得到原来的 CFrame,就像从未进行过乘法运算一样。
local cf = CFrame.new(1, 2, 3) * CFrame.Angles(math.rad(14), math.rad(72), math.rad(-32))
print(cf*CFrame.new())
print(CFrame.new()*cf)
1, 2, 3, 0.262061268, 0.163754046, 0.95105654, -0.319058299, 0.944782019, -0.0747579709, -0.910783052, -0.283851326, 0.299837857
1, 2, 3, 0.262061268, 0.163754046, 0.95105654, -0.319058299, 0.944782019, -0.0747579709, -0.910783052, -0.283851326, 0.299837857
CFrame * Vector3¶
我们已经知道 CFrame 实际上是 4x4 的矩阵,那么下面就让我们来看看如何将其与向量相乘。CFrame 与 Vector3 的乘法运算用矩阵形式表现是这样的。
因此我们可以编写下列函数
local function multiplycfv3(a, b)
local x, y, z, m11, m12, m13, m21, m22, m23, m31, m32, m33 = a:components()
local vx, vy, vz = b.x, b.y, b.z
local nx = m11*vx+m12*vy+m13*vz+x
local ny = m21*vx+m22*vy+m23*vz+y
local nz = m31*vx+m32*vy+m33*vz+z
return Vector3.new(nx, ny, nz)
end
下面让我们再次来进行测试。
local cf = CFrame.new(1, 2, 3) * CFrame.Angles(math.rad(14), math.rad(72), math.rad(-32))
local v3 = Vector3.new(5, 6, -12)
print(cf*v3)
print(multiplycfv3(cf, v3))
-8.11984825, 6.97049618, -6.85507774
-8.11984825, 6.97049618, -6.85507774
与 CFrame 间互乘不同,CFrame * Vector3 的乘法可以拆分为更加直观的形式。让我们来对表示法稍作调整。
是否注意到我们用来与 vx、vy 和 vz 相乘的向量?它们就是我们早些时候学习到的 right、up 和 back 向量!下面让我们来重写函数,反应这一知识。
local function multiplycfv3(a, b)
return a.p + b.x*a.rightVector + b.y*a.upVector - b.z*a.lookVector
end
这也有助于我们将运算的实际意义以视觉化方式呈现。
CFrame + / - Vector3¶
在 CFrame 上加上或减去 Vector3 是十分直观的运算。我们只需在 CFrame x、y 和 z 上加/减向量 x、y 和 z,而旋转方位则保持不变。
local function addcfv3(a, b)
local x, y, z, m11, m12, m13, m21, m22, m23, m31, m32, m33 = a:components()
return CFrame.new(x + b.x, y + b.y, z + b.z, m11, m12, m13, m21, m22, m23, m31, m32, m33);
end;
当然也需要进行测试。
local cf = CFrame.new(1, 2, 3) * CFrame.Angles(math.rad(14), math.rad(72), math.rad(-32))
local v3 = Vector3.new(5, 6, -12)
print(cf + v3)
print(addcfv3(cf, v3))
6, 8, -9, 0.262061268, 0.163754046, 0.95105654, -0.319058299, 0.944782019, -0.0747579709, -0.910783052, -0.283851326, 0.299837857
6, 8, -9, 0.262061268, 0.163754046, 0.95105654, -0.319058299, 0.944782019, -0.0747579709, -0.910783052, -0.283851326, 0.299837857
CFrame 的逆运算¶
CFrame 的逆运算对于大多数人来说都有一定难度。我们将不会在本文中介绍具体如何进行逆运算,而是会着重解说逆运算的使用方法。
我们在 CFrame 互乘章节最后曾提到,乘法的乘数并非总是能够互换。对于乘以派生于自身 CFrame 的 CFrame 逆运算来说,这一条并不适用。无论开发者自前还是自后用 CFrame 乘以其逆运算,始终都会返回身份识别 CFrame!
local cf = CFrame.new(1, 2, 3) * CFrame.Angles(math.pi/2, 0, 0)
print(cf*cf:inverse())
print(cf:inverse()*cf)
0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1
0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1
运用 CFrame 逆运算的窍门就是写出等式,然后运用我们所掌握的身份识别 CFrame 和 CFrame 乘数不可互换属性等相关知识。让我们来看几个示例。
还原为初始值¶
假设我们有两个 CFrame,并将其相乘得出一个新的 CFrame。
local cf1 = CFrame.new(1, 2, 3) * CFrame.Angles(math.pi/3, math.pi/6, 0)
local cf2 = CFrame.new(-4, 5, 7.2) * CFrame.Angles(0, math.pi/7, -math.pi/3)
local cf = cf1 * cf2
假设仅给定了 cf 和 cf1,而我们希望求出 cf2。要如何才能做到?要着手解决该问题,我们就要看一下 cf 的等式。
cf = cf1 * cf2
然后我们就可以应用自己掌握的逆运算知识来求得 cf2。
cf = cf1 * cf2
cf1:inverse() * cf = cf1:inverse() * cf1 * cf2 -- 自前以 cf1:inverse() 相乘等式两边
cf1:inverse() * cf = CFrame.new() * cf2 -- 注意 cf:inverse() * cf = identityCFrame
cf1:inverse() * cf = cf2 -- 注意 identityCFrame * cf = cf
毫无疑问,我们能通过测试验证这一结果。
local cf1 = CFrame.new(1, 2, 3) * CFrame.Angles(math.pi/3, math.pi/6, 0)
local cf2 = CFrame.new(-4, 5, 7.2) * CFrame.Angles(0, math.pi/7, -math.pi/3)
local cf = cf1*cf2
print(cf2)
print(cf1:inverse() * cf)
-4, 5, 7.19999981, 0.450484395, 0.780261934, 0.433883756, -0.866025448, 0.49999997, 0, -0.216941863, -0.375754386, 0.90096885
-4, 5.00000143, 7.19999933, 0.450484395, 0.780261934, 0.433883697, -0.866025507, 0.5, -2.98023224e-08, -0.216941863, -0.375754386, 0.90096879
请注意,输出结果中的细微变化是由于浮点数学运算的误差所导致
假设我们已知 cf2 和 cf,但 cf1 未知。要求解该问题,我们也可以采用相似的步骤。
cf = cf1 * cf2
cf * cf2:inverse() = cf1 * cf2 * cf2:inverse() -- 自后以 cf2:inverse() 相乘等式两边
cf * cf2:inverse() = cf1 * CFrame.new() -- 注意 cf * cf:inverse() = identityCFrame
cf * cf2:inverse() = cf1 -- 注意 cf * identityCFrame = cf
再次进行验证测试。
local cf1 = CFrame.new(1, 2, 3) * CFrame.Angles(math.pi/3, math.pi/6, 0)
local cf2 = CFrame.new(-4, 5, 7.2) * CFrame.Angles(0, math.pi/7, -math.pi/3)
local cf = cf1*cf2
print(cf1)
print(cf * cf2:inverse())
1, 2, 3, 0.866025388, 0, 0.5, 0.433012724, 0.49999997, -0.75, -0.249999985, 0.866025448, 0.433012664
1.00000048, 2.00000048, 3.00000095, 0.866025329, -2.98023224e-08, 0.49999997, 0.433012664, 0.5, -0.75, -0.25000003, 0.866025507, 0.433012664
请注意,结果中的细微变化是由于浮点数学运算的误差所导致
或许开发者心中会有这样的疑问:自前与自后相乘究竟为何如此重要?要得知原因所在,让我们来有意识地逐步研究 cf 自前乘以 cf2:inverse() 的整个运算过程,并观察最终的结果。
cf = cf1 * cf2
cf2:inverse() * cf = cf2:inverse() * cf1 * cf2
-- 由于不知道 cf2:inverse() * cf1 = ??? 的答案,所以
cf2:inverse() * cf = ???
最终的结论就是,顺序是十分重要的,而且我们对等式一侧的处理也必须应用于另一侧,不管是否自前或自后相乘都包括在内!
门的旋转¶
假设我们希望通过 CFrame 实现门的开合。这对于部分学习 CFrame 的人来说可能会十分困难,因为当我们对某一部件的 CFrame 使用 CFrame.Angles
函数并更新时,它会从中心开始转动。
local door = game.Workspace.Door
game:GetService("RunService").Heartbeat:connect(function(dt)
door.CFrame = door.CFrame * CFrame.Angles(0, math.rad(1)*dt*60, 0)
end)
理想情况下,我们自然希望门能围绕某种铰链转动。也就是说,我们需要寻找一种方法来将我们的铰链作为旋转的中心。我们知道自己能够以类似早先旋转门的方式来旋转铰链。
local door = game.Workspace.Door
local hinge = game.Workspace.Hinge
game:GetService("RunService").Heartbeat:connect(function(dt)
hinge.CFrame = hinge.CFrame * CFrame.Angles(0, math.rad(1)*dt*60, 0)
end)
如果我们能设法计算出门相对于未旋转铰链的偏移值,就可以将该偏移值应用到旋转的铰链,并得出旋转门的 CFrame。换言之,我们必须在以下运算中求得偏移值:
hinge.CFrame * offset = door.CFrame
求得偏移值的关键之处,就是使用逆运算!切记,我们对等式一侧的处理也必须应用于另一侧。
hinge.CFrame * offset = door.CFrame -- want to get rid of hinge.CFrame on left side
hinge.CFrame:inverse() * hinge.CFrame * offset = hinge.CFrame:inverse() * door.CFrame -- 因为乘数无法互换,进行自前相乘
CFrame.new() * offset = hinge.CFrame:inverse() * door.CFrame -- cf:inverse() * cf = CFrame.new()
offset = hinge.CFrame:inverse() * door.CFrame -- CFrame.new() * cf = cf
求出偏移值后,让我们将其应用到旋转铰链上吧!
local door = game.Workspace.Door
local hinge = game.Workspace.Hinge
local offset = hinge.CFrame:inverse() * door.CFrame; -- 旋转前首先进行偏移
game:GetService("RunService").Heartbeat:connect(function(dt)
hinge.CFrame = hinge.CFrame * CFrame.Angles(0, math.rad(1)*dt*60, 0) -- 旋转铰链
door.CFrame = hinge.CFrame * offset -- 将偏移应用至旋转铰链
end)
亲自尝试:接合¶
接合会受到以下条件约束。
weld.Part0.CFrame * weld.C0 = weld.Part1.CFrame * weld.C1
请运用我们之前掌握的逆运算知识,尝试求出 Weld.C0 和 Weld.C1。完成自我测试前尽量不要参考答案。
--解决方法:Weld.C0:
weld.Part0.CFrame * weld.C0 = weld.Part1.CFrame * weld.C1
weld.Part0.CFrame:inverse() * weld.Part0.CFrame * weld.C0 = weld.Part0.CFrame:inverse() * weld.Part1.CFrame * weld.C1
CFrame.new() * weld.C0 = weld.Part0.CFrame:inverse() * weld.Part1.CFrame * weld.C1
weld.C0 = weld.Part0.CFrame:inverse() * weld.Part1.CFrame * weld.C1
--解决方法:C1:
weld.Part0.CFrame * weld.C0 = weld.Part1.CFrame * weld.C1
weld.Part1.CFrame:inverse() * weld.Part0.CFrame * weld.C0 = weld.Part1.CFrame:inverse() * weld.Part1.CFrame * weld.C1
weld.Part1.CFrame:inverse() * weld.Part0.CFrame * weld.C0 = CFrame.new() * weld.C1
weld.Part1.CFrame:inverse() * weld.Part0.CFrame * weld.C0 = weld.C1
CFrame 方法¶
在最后一节中,我们将探讨每种变换方法及对这些方法的部分直观理解。
CFrame:ToObjectSpace()¶
等同于 CFrame:inverse() * cf
其实之前尝试实现门的旋转时,我们就已经从求取偏移值的过程中了解到了该方法的作用。此方法能计算出 CFrame 需要从 CFrame
中取得的偏移值,用以求出 cf
开发者可以通过以下函数轻松对其进行验证:
CFrame * CFrame:toObjectSpace(cf)
CFrame * CFrame:inverse() * cf
identityCFrame * cf
cf
CFrame:ToWorldSpace()¶
等同于 CFrame * cf
此方法只是进行 CFrame 乘法,可能会让人觉得有些平凡无奇。不过,该方法的名称可能有助于我们更为直观地理解乘法运算的实际概念。我们在 CFrame:toObjectSpace(cf)
方法中看到,它返回的是两个 CFrame 之间的偏移值。我们还注意到,当我们用 CFrame
乘以偏移值后,最终结果是 cf
。那么,我们将两个 CFrame 相乘时,实际上就是将第二个 CFrame 当做了偏移值。
local cf = CFrame.new(1, 2, 3) * CFrame.Angles(math.rad(14), math.rad(72), math.rad(-32))
local offset = CFrame.new(0, 0, -10) -- 向前 10 格偏移
print(cf:toWorldSpace(offset)) -- 向前 10 格偏移。记住 cf 的前方为 cf.lookVector
-8.51056576, 2.74757957, 0.00162148476, 0.262061268, 0.163754046, 0.95105654, -0.319058299, 0.944782019, -0.0747579709, -0.910783052, -0.283851326, 0.299837857
CFrame:PointToObjectSpace()¶
等同于 CFrame:inverse() * v3
此方法会在 3D 空间当中取特定点并使之与 CFrame.p
相对应,然后将其换算为偏移值。
此方法的替代方法为 (CFrame - CFrame.p):inverse() * (v3 - CFrame.p)
local cf = CFrame.new(1, 2, 3) * CFrame.Angles(math.rad(14), math.rad(72), math.rad(-32))
local v3 = Vector3.new(10, 10, 15)
print(cf:pointToObjectSpace(v3))
print((cf - cf.p):inverse() * (v3 - cf.p))
-11.123312, 5.62582684, 11.5594997
-11.123312, 5.62582684, 11.5594997
CFrame:PointToWorldSpace()¶
等同于 CFrame * v3
由于我们早已探讨过了 CFrame * v3
的直观效果,因此除去已知内容外并无太多内容需要赘述。但需要再次进行提醒的是:此方法相当于在没有旋转方位的情况下应用偏移值。
local cf = CFrame.new(1, 2, 3) * CFrame.Angles(math.rad(14), math.rad(72), math.rad(-32))
local v3 = Vector3.new(10, 10, 15)
print(cf * cf:pointToObjectSpace(v3))
10.000001, 10, 15.0000019
请注意,结果中的细微变化是由于浮点数学运算的误差所导致
CFrame:VectorToObjectSpace()¶
等同于 (CFrame-CFrame.p):inverse() * v3
此方法十分类似于 CFrame:pointObjectSpace(v3)
方法的替代形式。最主要的区别就是 v3
不再减去 CFrame.p
。换而言之,两者步骤非常相似,唯一的区别就是不会使 v3
与 CFrame.p
相对应,而是默认之前所输入的 v3 已为相对数值。
local cf = CFrame.new(1, 2, 3) * CFrame.Angles(math.rad(14), math.rad(72), math.rad(-32))
print(cf:vectorToObjectSpace(cf.rightVector))
1.00000012, 2.98023224e-08, 0
请注意,结果中的细微变化是由于浮点数学运算的误差所导致
我们可以看到,其结果等于或约等于 Vector3.new(1, 0, 0),即 identityCFrame 的 rightVector。理想情况下,这两者完全相等,但正如上文所提到的,产生细微区别的原因是由于浮点数学运算的误差。
CFrame:VectorToWorldSpace()¶
等同于 (CFrame-CFrame.p) * v3
此方法的替代形式是 CFrame * v3 - CFrame.p
。也就是说,此方法其实与 CFrame:pointWorldSpace(v3)
相差无几,但并不会添加 CFrame.p(如我们在 CFrame * v3 一节中所学到的)。
local cf = CFrame.new(1, 2, 3) * CFrame.Angles(math.rad(14), math.rad(72), math.rad(-32))
print(cf:vectorToWorldSpace(Vector3.new(1, 0, 0)))
print(cf * Vector3.new(1, 0, 0) - cf.p)
print(cf.rightVector)
0.262061268, -0.319058299, -0.910783052
0.262061238, -0.319058299, -0.910783052
0.262061268, -0.319058299, -0.910783052
***Roblox官方链接:CFrame 数学运算