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 Axes

CFrame * CFrame

CFrame 实际上是采用以下形式的 4x4 矩阵:

CFrameMath_cf4x4.png

也就是说,我们只需将两个 4x4 的矩阵相乘,就能轻松地将两个 CFrame 相乘!

CFrameMath_cf4x4Multiply2.png

这样我们就能编写出用于将两个 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 的乘法运算用矩阵形式表现是这样的。

CFrameMath_cfv3Multiply.png

因此我们可以编写下列函数

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 的乘法可以拆分为更加直观的形式。让我们来对表示法稍作调整。

CFrameMath_cfv3Multiply2.png

是否注意到我们用来与 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

这也有助于我们将运算的实际意义以视觉化方式呈现。

CFrameMath_cfv3MultiplyVisual.png

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 函数并更新时,它会从中心开始转动。

CFrameMath_doorRotate1.gif

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)

理想情况下,我们自然希望门能围绕某种铰链转动。也就是说,我们需要寻找一种方法来将我们的铰链作为旋转的中心。我们知道自己能够以类似早先旋转门的方式来旋转铰链。

CFrameMath_doorRotate2.gif

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

求出偏移值后,让我们将其应用到旋转铰链上吧!

CFrameMath_doorRotate3-compressor.gif

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

CFrameMath_pointToObjectSpace.gif

此方法会在 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。换而言之,两者步骤非常相似,唯一的区别就是不会使 v3CFrame.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 数学运算