Setting a CFrame to another CFrame on a per-axis basis (using dot & cross product)

Hello, I’m currently trying to make a snippet of code that will take one CFrame and perfectly correct it using Vector maths so that it perfectly faces another CFrame.

Context

The reason I wouldn’t simply set the CFrame to its counterpart and rotate it about an axis is because I eventually plan for this to be a part of a robust building-piece snapping system; the end goal being for the acted-upon “snap points” (represented by the CFrames) to be snapped to one-another among arbitrary angular increments (i.e, snap points of two wall pieces snapping to connect to each other at flush or 90-degree angles). So in essence, say a piece is being placed and it is going to be snapped to another piece; the already-placed piece’s snap point would ensure that the new wall was flush with it’s floor plane, but the wall could still be freely rotated on a single axis relative to the point. It’s a bit hard to explain but I hope that explains it.

Tl;dr I would like full control over each axis that is set so that I can choose to only correct certain ones.

This is what I have tried previously, using Cross and Dot products sequentially with each axis in an arbitrary order.

-- cframe1: The cframe that will be corrected
-- cframe2: The cframe that cframe1 will be corrected to
function module:SnapPoints(cframe1, cframe2)
	local newCf = CFrame.new(cframe2.p) * (cframe1 - cframe1.p)
	
	local applySnap = function(vectorName, axisName)
		local currentVector = newCf[vectorName]
		local desiredVector = cframe2:inverse()[vectorName]
		
		local cross = currentVector:Cross(desiredVector)
		local theta = math.acos(currentVector:Dot(desiredVector))
		
		if cross.magnitude ~= 0 then
			local axis = cross.unit
			newCf *= CFrame.fromAxisAngle(axis, theta)
		end
	end
	
	applySnap("rightVector", "x")
	applySnap("lookVector", "z")
	applySnap("upVector", "y")
	
	return newCf
end

It works when cframe1 is only off-grid by a single axis, but when multiple axes need to be corrected, it fails to do so properly.

I have no idea if using Cross & Dot Product are the best way to go about this, or if even going about it on a per-axis way is a good idea. I’m not sure why this solution isn’t working as I intend, since it both gets an axis of rotation and an angle to follow, and applies it to the CFrame for every axis. Vector math has always been crazy to me, so any possible explanation would be appreciated!

2 Likes

I would not divide into three axes like this. What if the floor plane is itself tilted?

The context section helps a lot, but could you explain exactly how you envision this working, and what part this function calculates?

Say you’re placing and rotating a single part against, say, a baseplate that’s tilted slightly off axis. What inputs does this function get, and what should it return?

2 Likes

I envision this being used as a layer on top of a set of movement tools used to move/place pieces.
When a piece is moving, it will take its snap points, which would be elements of the model, and check to see if they are in the range of any other snap points on the map. If it finds one that is valid (in range, of the same type, etc) they would snap together.
To allow for more freedom of placement, the snapping would be axis-specific: like for example, the X and Z axes of some pieces may snap together, so the Y axis may rotate freely or with an increment, kind of like a hinge to allow for more interesting creations.

The inputs would be the CFrame of a the point that is trying to be snapped, and the point that it is trying to snap to respectively.
The return value would be the resulting snapped CFrame, where if the point was placed and a model was moved relative to it, it would appear to be attached to the second snap point.

Does that help explain anything?

2 Likes

Okay, so it sounds you’re basically re-implementing the Attachments and Constraints system, but with CFrame instead of physics?

First let me suggest ignoring the problem entirely: just unanchor parts (or use proxy parts that you only use to calculate snapping), set Anchored to false on them while you’re moving them, and let Roblox solve the snaps for you. If you set their CFrames on .Stepped, you can avoid them moving due to physics, but the constraints would still be solved every frame. You can enable/disable them as the draggers go in/out of snapping range. Just a thought.

Assuming you want to go forward with your approach for the rest of this.

Your description says the snapping would be axis-specific, but your code deals with all three.

I would just do what HingeConstraint does, and define the X-axis as the axis you want to align. If you want the Attachments (or “snap points”) to align differently, you just rotate the snap points.

This simplifies your life, and now your task just becomes:

Rotate CFrame A the least amount possible so that its RightVector aligns with the RightVector of CFrame B.

Which is a much easier problem to think about:

-- Returns `cf`, modified such that it occupies the same position
-- as `snapTo` and that its RightVector is aligned to
-- `snapTo.RightVector`
local function SolveHingeConstraint(cf: CFrame, snapTo: CFrame)
	local right = snapTo.RightVector
	local up = right:Cross(cf.LookVector)

	return CFrame.fromMatrix(snapTo.Position, right, up)
end

This gives this result (green is the “snapTo” target, red is the “cf” original, gray is the returned result):

The RightVectors of the CFrames are also shown in white.

1 Like

Just a footnote here:

If you are aligning two axes, you are aligning all three. Saying you want the UpVector and RightVector to align is the same as saying you want the rotations to exactly match.

This is a good way to think about this that I didn’t consider before. I definitely shouldn’t underestimate Roblox’s built-in instances for this kind of situation. Thank you!