Setting a CFrame using an Orientation that has 90 or 270 degrees is just ever so slightly off

When I set the Orientation of a BasePart to a vector, all of whose elements are divisible by 90 but not 180, the resulting rotation of the BasePart.CFrame is ever so slightly off:

local Part = Instance.new("Part")
Part.Orientation = Vector3.new(90)
print(Part.CFrame)

Result:

0, 0, 0, 1, 0, 0, 0, -4.37113883e-08, -1, 0, 1, -4.37113883e-08

I suspect this stems from some rounding error in the intermediate calculation even though the starting orientation in degrees and the expected resultant CFrame can both be represented exactly in floating point.

Possible fix: Using sinpi(x) instead of sin(M_PI*x) in the calculation, or similar.

Expected behavior

Expected result:

0, 0, 0, 1, 0, 0, 0, 0, -1, 0, 1, 0
2 Likes

I do also experience this on my upcoming project, and I do somewhat agree this is a bug:

Hi,

Thanks for bringing this up. What you see is due to floating point errors…
Not much we can do here but thank you nonetheless for bring it up!

Best,
M0bsterLobster

Technically there are possible fixes, such as using implementation-specific __sinpi functions where available, or handling those cases separately.

This is the code the Roblox Studio draggers use to fixup very small floating point error into precise orthogonal rotations if you want a workaround:

local function fixSlightlyOffgridOrientation(cf: CFrame): CFrame
	-- Get the components
	local x, y, z, r00, r01, r02, r10, r11, r12, r20, r21, r22 = cf:GetComponents()

	-- Restore any values which are very close to zero to zero.
	-- This code is fairly ugly but this is used on a performance critical
	-- codepath so it must be maximally unrolled rather than being restructured
	-- into some form of loop over a table.
	-- Typically the values to be corrected will be ~3e-8 so we only need a
	-- very small threshold and keeping the threshold especially small avoids
	-- impacting any actual use cases.
	local THRESHOLD = 1e-5
	local clamped = false
	if math.abs(r00) < THRESHOLD and r00 ~= 0 then
		r00 = 0
		clamped = true
	end
	if math.abs(r01) < THRESHOLD and r01 ~= 0 then
		r01 = 0
		clamped = true
	end
	if math.abs(r02) < THRESHOLD and r02 ~= 0 then
		r02 = 0
		clamped = true
	end
	if math.abs(r10) < THRESHOLD and r10 ~= 0 then
		r10 = 0
		clamped = true
	end
	if math.abs(r11) < THRESHOLD and r11 ~= 0 then
		r11 = 0
		clamped = true
	end
	if math.abs(r12) < THRESHOLD and r12 ~= 0 then
		r12 = 0
		clamped = true
	end
	if math.abs(r20) < THRESHOLD and r20 ~= 0 then
		r20 = 0
		clamped = true
	end
	if math.abs(r21) < THRESHOLD and r21 ~= 0 then
		r21 = 0
		clamped = true
	end
	if math.abs(r22) < THRESHOLD and r22 ~= 0 then
		r22 = 0
		clamped = true
	end
	if clamped then
		-- If we restored anything to zero, also restore things close to 1.
		-- We should only do this if something was clamped to zero because
		-- values close to 1 are much more sensitive. With a small angle theta
		-- the values above will change by ~theta but the values below will
		-- only change by 1-cos(theta) which asymtotically approaches zero
		-- for the small theta.
		if math.abs(r00 - 1) < THRESHOLD then
			r00 = 1
		elseif math.abs(r00 + 1) < THRESHOLD then
			r00 = -1
		end
		if math.abs(r01 - 1) < THRESHOLD then
			r01 = 1
		elseif math.abs(r01 + 1) < THRESHOLD then
			r01 = -1
		end
		if math.abs(r02 - 1) < THRESHOLD then
			r02 = 1
		elseif math.abs(r02 + 1) < THRESHOLD then
			r02 = -1
		end
		if math.abs(r10 - 1) < THRESHOLD then
			r10 = 1
		elseif math.abs(r10 + 1) < THRESHOLD then
			r10 = -1
		end
		if math.abs(r11 - 1) < THRESHOLD then
			r11 = 1
		elseif math.abs(r11 + 1) < THRESHOLD then
			r11 = -1
		end
		if math.abs(r12 - 1) < THRESHOLD then
			r12 = 1
		elseif math.abs(r12 + 1) < THRESHOLD then
			r12 = -1
		end
		if math.abs(r20 - 1) < THRESHOLD then
			r20 = 1
		elseif math.abs(r20 + 1) < THRESHOLD then
			r20 = -1
		end
		if math.abs(r21 - 1) < THRESHOLD then
			r21 = 1
		elseif math.abs(r21 + 1) < THRESHOLD then
			r21 = -1
		end
		if math.abs(r22 - 1) < THRESHOLD then
			r22 = 1
		elseif math.abs(r22 + 1) < THRESHOLD then
			r22 = -1
		end
		return CFrame.new(x, y, z, r00, r01, r02, r10, r11, r12, r20, r21, r22)
	else
		return cf
	end
end
2 Likes

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.