A Couple of Advanced CFrame Tricks

So I wanted to make this post to share a few niche, but useful CFrame tricks along with examples.

Be forewarned I’m going to start talking about Quaternions so if you’re not versed in that you’ll probably want to read up on that.

Rotation between two vectors CFrame

The first thing I want to talk about is calculating a CFrame that represents the shortest rotational path between two vectors. So given two unit vectors u and v can we calculate a CFrame that gives us the shortest rotational path between the two?

In other words a CFrame that would fulfill the following:

getRotationBetween(u, v) * u == v

Well first let’s see how we can deal with this problem without quaternions. There are three cases we have to watch out for:

  1. When the u and v vectors are the same i.e. u == v, then we know the shortest rotational path is no rotation at all!

  2. If the u and v are complete opposites i.e. u == -v, then we choose an arbitrary axis and rotate by 180 degrees.

  3. When u and v are different, but not complete opposites we use the cross product to get the axis of rotation and the dot product to get the angle of rotation. We can then plug these into the CFrame.fromAxisAngle constructor to get the rotation between the two vectors as a CFrame.

Putting this into code:

local function getRotationBetween(u, v, axis)
    local dot = u:Dot(v)
    if (dot > 0.99999) then
        -- situation 1
        return CFrame.new()
    elseif (dot < -0.99999) then
        -- situation 2
        return CFrame.fromAxisAngle(axis, math.pi)
    end
    -- situation 3
    return CFrame.fromAxisAngle(u:Cross(v), math.acos(dot))
end

Now this is fine and all, but there’s a more elegant solution (in my opinion at least) with quaternions.

We know that we could convert an axis angle rotation to a quaternion by doing the following:

theta = math.acos(u:Dot(v))
axis = u:Cross(v).Unit

qw = math.cos(theta / 2)
qx = math.sin(theta / 2) * axis.x
qy = math.sin(theta / 2) * axis.y
qz = math.sin(theta / 2) * axis.z

We also know that due to the magnitudes of the cross and dot product that:

u:Dot(v) = |u|*|v|*cos(theta) = cos(theta)
u:Cross(v).x = |u|*|v|*sin(theta) * axis.x = sin(theta) * axis.x
u:Cross(v).y = |u|*|v|*sin(theta) * axis.y = sin(theta) * axis.y
u:Cross(v).z = |u|*|v|*sin(theta) * axis.z = sin(theta) * axis.z

So you’ll note then that if I just went ahead and plugged the dot and cross products into the quaternion constructor I’d get a rotation that represents double the rotation along the correct axis! So the question becomes, is there a way to half this rotation amount? Yes, there are a couple.

A lazy way would be to (s)lerp half-way:

local function getRotationBetween(u, v, axis)
    local dot, uxv = u:Dot(v), u:Cross(v)
    return CFrame.new():Lerp(CFrame.new(0, 0, 0, uxv.x, uxv.y, uxv.z, dot), 0.5)
end

A more efficient way would be to recognize that since we’re dealing with a unit quaternion which is a point on a 4D hypersphere we can just add our quaternion to the un-rotated state and normalize. This might be somewhat hard to visualize in 4D so let’s use an image of a 3D sphere to hopefully get the message across.

In this case q_r is our double rotation quaternion and q_u is an unrotated quaternion. If we add them together we get quaternion w.

This isn’t a unit quaternion however so we have to normalize it to w_n.

So taking into account then that our double rotated quaternion can be represented as q_r = [u:Dot(v), u:Cross(v)] and the unrotated quaternion as q_u = [1, Vector3.new(0, 0, 0)] then adding them together we get: q_r + q_u = [1 + u:Dot(v), u:Cross(v)]. We can just plug this in directly to the CFrame quaternion constructor as it will normalize the result for us.

Now one thing to note about an edge case that we weren’t able to catch with the lerp method. Say that u == -v. In that case our added quaternion is: q_r + q_u = [0, Vector3.new(0, 0, 0)]. Of course this can’t be normalized so we don’t have a valid rotation when there’s a 180 degree difference. As a result we’ll need to again bring over a backup axis.

local function getRotationBetween(u, v, axis)
    local dot, uxv = u:Dot(v), u:Cross(v)
    if (dot < -0.99999) then return CFrame.fromAxisAngle(axis, math.pi) end
    return CFrame.new(0, 0, 0, uxv.x, uxv.y, uxv.z, 1 + dot)
end

Well there we have it, a function to get the CFrame rotation between two vectors. So what’s it useful for?

Arguably one of the more straight forward uses is that it provides a really quick and robust way to slerp unit vectors.

local rotation = getRotationBetween(u, v, axis)

for i = 0, 1.01, 0.01 do
    local v = CFrame.new():Lerp(rotation, i) * u
end

2019-08-24_10-31-44
2019-08-24_10-32-29

Another neat use for this I actually discovered while talking to @Quenty. This function comes in handy when you’re trying to find a surface CFrame aligned with the surface edges.

The idea is as follows:

  1. Pick a static surface normal that you’ll be able to use for any part.

  2. Find the rotation between that universal surface normal and the arbitrary one you supply. Take note that we can also pick a static axis vector to rotate around given the 180 degree case since we know the value of the universal surface normal.

  3. Multiply this rotation against part’s CFrame.

  4. Rotate this by some constant (if you wish) such that the axes running parellel to the surface are to your preference.

So in my case I chose to use Vector3.new(0, 1, 0) as the universal normal and as such I picked Vector3.new(0, 0, 1) as the axis b/c I know it’s orthogonal to the universal normal.

-- makes it so the RightVector and UpVector run parellel to the surface and LookVector = surface normal
local EXTRASPIN = CFrame.fromEulerAnglesXYZ(math.pi/2, 0, 0)

local function getSurfaceCF(part, lnormal)
    local transition = getRotationBetween(Vector3.new(0, 1, 0), lnormal, Vector3.new(0,0, 1))
    return part.CFrame * transition * EXTRASPIN
end

Another example of this function is again using the slerp functionality to transition smoothly to a custom camera up vector. You can read about that here:

Swing-Twist decomposition

Now for a new question. Is it possible to find the spin around an arbitrary axis?

Say we have some rotation. We know that any rotation can be written as a combination of some arbitrary rotation and some amount of twisting. Commonly this is called a swing-twist decomposition.

rotation = swing * twist

So the question is can we some how decompose a CFrame to find the swing and twist values given we pick an arbitrary twist axis?

Well there’s a couple approaches we can come at this from. We can use our function from above to find the amount we swing from our twist axis to the end rotation and then use the inverse to solve for the twist.

local function swingTwist(cf, direction)
	local swing = CFrame.new()
	local rDirection = cf:VectorToWorldSpace(direction)
    if (rDirection:Dot(direction) > -0.99999) then
        -- we don't need to provide a backup axis b/c it will nvr be used
		swing = getRotationBetween(direction, rDirection, nil)
	end
	-- cf = swing * twist, thus...
	local twist = swing:Inverse() * cf
	return swing, twist
end

This is alright, but it’s dependent on the getRotationBetween function we found earlier. That’s fine, but ideally we could have these functions be independent. Luckily we can figure out a more elegant solution by looking at the composition as quaternions.

We want to solve for qs and qt which when multiplied together give us our final rotation q. We know the components of q and we know the direction of the twist axis d.

In summary:

q = [w, v]
qs = [ws, vs]
qt = [wt, vt]

q = qs * qt

d = unit twist axis

By definition we know a few results of using the dot product so let’s write them out for later use:

vs . vt = 0 -- the twist and swing axis are orthogonal by definition
vs . d = 0 -- d is the same direction as vt thus by same logic as above these are orthogonal
vt . d = |vt||d|*cos(0) = |vt| -- since d and vt go in same direction angle between them is 0
vs:Cross(vt) . d = 0 -- vt and d are in same direction thus the cross and d must be orthogonal

Now if we use the quaternion rules of multiplication and simplify what we can with the above:

q = qs * qt = [ws*wt - vt.vs, ws*vt + wt*vs + vs:Cross(vt)]
            = [ws*wt, ws*vt + wt*vs + vs:Cross(vt)]

Thus,
w = ws*wt
v = ws*vt + wt*vs + vs:Cross(vt)

Now if we project q onto the unit twist axis we get a new quaternion which we’ll call qp

qp = q projected onto d

qp = [w, (v.d)*d]
   = [w, (ws*(vt . d) + wt*(vs . d) + vs:Cross(vt) . d)*d]
   = [w, ws*|vt|*d]
   = [w, ws*vt]
   = [ws*wt, ws*vt]

Now if we normalize qp we’ll see:

qp / |qp| = [ws*wt, ws*vt] / sqrt(ws^2*wt^2 + ws^2*|vt|^2)
          = ws*[wt, vt] / ws*sqrt(wt^2 + |vt|^2)

We know that sqrt(wt^2 + |vt|^2) = 1 because the twist quaternion is a unit quaternion by definition so therefore:

qp / |qp| = [wt, vt]

Now that we have qt solving qs is very easy we simply rearrange the original q = qs * qt equation with inverses to solve for qs

q = qs * qt
q * qt:Inverse() = qs

As for code you might thing we have to normalize qp but since the CFrame constructor does that for us already so we can just plug in the values unchanged.

local function swingTwist(cf, direction)
	local axis, theta = cf:ToAxisAngle()
	-- convert to quaternion
	local w, v = math.cos(theta/2),  math.sin(theta/2)*axis

	-- plug qp into the constructor and it will be normalized automatically
	local proj = v:Dot(direction)*direction
	local twist = CFrame.new(0, 0, 0, proj.x, proj.y, proj.z, w)

	-- cf = swing * twist, thus...
	local swing = cf * twist:Inverse()

	return swing, twist
end

Now, it’s important to note that by the nature of converting a quaternion to an axis angle, the rotation angle will be always be positive and it’s the axis that will flip.

This may not be ideal depending on what you’re trying to do, but it’s an easy fix. We just check the sign of the dot product of the input direction against the end rotation’s axis.

local function twistAngle(cf, direction)
    local axis, theta = cf:ToAxisAngle()
    local w, v = math.cos(theta/2),  math.sin(theta/2)*axis
	local proj = v:Dot(direction)*direction
    local twist = CFrame.new(0, 0, 0, proj.x, proj.y, proj.z, w)
    local nAxis, nTheta = twist:ToAxisAngle()
    return math.sign(v:Dot(direction))*select(2, twist:ToAxisAngle())
end

Okay, so why is this useful?

Well, so far I’ve mostly found use in the twist aspect of the decomposition. It allows me to find out how much a CFrame has spun around an arbitrary axis. This is useful for things such as matching the camera’s rotation to a spinning part you’re standing on.

For instance, in the custom camera up place which I linked above we can add:

CameraModule.LastSpinPart = game.Workspace.Terrain
CameraModule.SpinPartPrevCF = CFrame.new()
CameraModule.SpinCFrame = CFrame.new()
CameraModule.SumDelta = 0

-- update this method elsewhere to get the part we're standing on for example
function CameraModule:GetSpinPart()
	return game.Workspace.Terrain
end

function CameraModule:CalculateSpin()
	local spinPart = self:GetSpinPart()
	local rCF = spinPart.CFrame - spinPart.CFrame.p
	local prCF = self.SpinPartPrevCF - self.SpinPartPrevCF.p
	
    local direction = rCF:VectorToObjectSpace(self.UpVector)
    
    -- get the angular difference between current and last rotation around the camera up axis
    -- multiply by sign of y in case camera is upside down
    local delta = twistAngle(prCF:Inverse() * rCF, direction) * math.sign(self.UpVector.y)
    
    -- if we switch part we're standing on then we shouldn't rotate this frame
	if (spinPart ~= self.LastSpinPart) then
		delta = 0
	end

	self.SumDelta = self.SumDelta + delta
	self.SpinCFrame = CFrame.Angles(0, self.SumDelta, 0)
	self.SpinPartPrevCF = spinPart.CFrame
	self.LastSpinPart = spinPart
end

function CameraModule:Update(dt)
	if self.activeCameraController then
		local newCameraCFrame, newCameraFocus = self.activeCameraController:Update(dt)
		self.activeCameraController:ApplyVRTransform()

		self:CalculateRotationCFrame() -- used for custom up vector
		self:CalculateSpin()
		
		local offset = newCameraFocus:Inverse() * newCameraCFrame
        newCameraCFrame = newCameraFocus * self.SpinCFrame * self.RotationCFrame * offset
        -- rest of stuff from custom up place...

As I mentioned above, if you update the :GetSpinPart() method to what the character is standing on you get a result like so.

When most people attempt to do this type of thing they base it on the part’s delta CFrame as opposed to the delta angle. This can cause issues with things that wobble, but you’ll note if you test yourself that our solution solves that! :grin:

Another useful thing we can use this decomposition for is finding the center of rotation around an axis based purely off a delta CFrame.

CustomCamera.rbxl (143.2 KB)

Say we have a spinning platform like so:

2019-08-24_14-35-59

We know the center of rotation is where the motor is, but finding it out with code is a whole different story.

Normally we are trying to solve for P in this image given we know cfA and cfB:

2019-08-24_14-44-52

Normally we can’t get very far because that’s not enough information to solve for P or L. However, since we can get the delta angle around an axis we actually have one more piece of info that makes solving this possible.

2019-08-24_14-47-16

Now, with theta, solving is as simple as using the law of cosines and noting that the two angles at cfA and cfB must be the same.

local function findCenterOfRotation(cfA, cfB, axis)
	local cf = cfB:Inverse() * cfA
	local theta = twist(cf, axis)
	local v = cfA.p - cfB.p
	
	local vdot = v:Dot(v)
	local alpha = (math.pi - theta)/2
	local length = vdot > 0 and math.sqrt(0.5 * vdot / (1 - math.cos(theta))) or 0
    
	-- rotate v around axis by alpha, normalize, and set new length as L
	local point = vdot > 0 and cfB.p + (CFrame.fromAxisAngle(axis, alpha) * v).unit * length or cfA.p
	
	return point
end

Applying this to our original spinning platform:

local lastSpinCF = spin.CFrame

game:GetService("RunService").Heartbeat:Connect(function(dt)
	local point = findCenterOfRotation(spin.CFrame, lastSpinCF, AXIS)
	center.CFrame = CFrame.new(point)
	lastSpinCF = spin.CFrame
end)

2019-08-24_14-50-16

center of rotation.rbxl (21.8 KB)


Well, those are two very specific, but advanced CFrame tricks. Hope they end up helping you out in some way as you dev.

Enjoy!

304 Likes

I can’t understand much of this. But thanks for the great tricks. I’m barely starting out scripting myself and haven’t gotten to C-Frames yet.
Can you elaborate exactly how this would work in a short sentence?
It’d be appreciated, but you don’t have to.

3 Likes

I don’t know about a short sentence. I’d say my attempt at a quick explanation was in the original post. However, I can take another run at explaining the output of these functions.

I think I can give a bit more insight into the swing-twist decomposition, but I don’t know if I can give any better example/explanation of the getRotationBetween function. Anyways, for now:

Say I have a block:

Now I rotate the block in some crazy weird way:

Now I’m picking an arbitrary vector which I want to know how much of the rotation twists around . In the gif below I represent this vector (direction) with the bright green line.

2019-08-24_17-26-58

That’s not a very easy question to answer is it? Regardless we solved it above so if we use the function we can get two CFrames; one for the twist and the the other for the rest (swing). When I combine them together I still get the same end result, but some times it can be useful to have them separate.

2019-08-24_17-31-50

You can see in the above gif we first do the swing rotation, there’s a slight pause, then we do the twist. This gets us to the final end result CFrame.

23 Likes

Another neat trick I just thought of recently (although Idk if it’d qualify as “advanced”)

We all know and love the CFrame.fromEulerAnglesXYZ and CFrame.fromEulerAnglesYXZ constructors, but that’s only a few of many ways to apply rotation axis order. We can write a function that will apply rotations in any order given we write it out.

local function fromEulerAngles(order, ...)
	local cf = CFrame.new()
	for i = 1, #order do
		local axis = Vector3.FromAxis(Enum.Axis[string.sub(order, i, i)])
		cf = cf * CFrame.fromAxisAngle(axis, select(i, ...))
	end
	return cf
end

Now we can do stuff like:

fromEulerAngles("XXY", math.pi, -math.pi, math.pi/2)
fromEulerAngles("YXY", math.pi, -math.pi, -math.pi)
fromEulerAngles("YYYYZ", 1, 1, 1, 1, 1)
-- etc, etc...

The function is useful enough on it’s own but we could use metatables and a module to make the whole thing play nice with the standard CFrame function library.

-- in a module script

local cframe = {}

local function fromEulerAngles(order, ...)
	local cf = CFrame.new()
	for i = 1, #order do
		local axis = Vector3.FromAxis(Enum.Axis[string.sub(order, i, i)])
		cf = cf * CFrame.fromAxisAngle(axis, select(i, ...))
	end
	return cf
end

setmetatable(cframe, {
	__index = function(t, k)
		if (CFrame[k]) then 
			return CFrame[k]
		elseif (type(k) == "string" and string.sub(k, 1, 15) == "fromEulerAngles") then
			local order = string.sub(k, 16)
			local func = function(...) return fromEulerAngles(order, ...) end
			cframe[k] = func
			return func
		end
	end
})

return cframe

-- in some other script

local CFrame = require(the_above_module)
print(CFrame.fromEulerAnglesXXY(math.pi, -math.pi, math.pi/2))

Again, hope that helps! Enjoy!

28 Likes

Thanks for this neat thread you Math Deity ;D
I’ll try to learn about Quaternions.

4 Likes

Hey egomoose, I really love your tutorials on these stuff. A lot of people can program, but not many people can work with math this well. I think you should compile a full on guide for stuff like this! A lot of your content is scattered in different forums and it would be cool to have a reliable source to find all of it.
Thanks!

4 Likes

I agree, but it’s tough.

A lot of my older stuff was all on the wiki, but that was eventually removed. I still have those backups but not in a format that would be easy to transfer over to another website (such as github or the devforum). This requirement to basically reformat my old articles not only takes a lot of time, but also tends to make me want to rewrite a lot of stuff too.

I personally enjoy writing new things so my dedication to “porting” old things over to a new platform is quite low.

Anyways, I hope that over time I’ll eventually get there. Thanks for the support! :+1:

9 Likes

Wow great tutorial, a lot of information i can use! Thanks so much!

3 Likes

Another quick thing I should mention about swing twist:

In the OP I talked about solving for q = qs * qt, so swing then twist. However, if you tried to solve for q = qt * qs you’d get the exact same results we got above. Surely this doesn’t make sense? That would imply that q = qs * qt = qt * qs, right?

Well, no… What you’d be forgetting to take into account is that due to qt being pre-multiplied our input direction vector must be object space relative to the axis-aligned identity CFrame whereas before it was object space to the end result CFrame.

-- where cfB is the end result CFrame
-- swing twist our direction vector is object space to the end CFrame
local swing, twist = swingTwist(cfB, cfB:VectorToObjectSpace(line.CFrame.UpVector))

-- twist swing our direction vector is object space to CFrame.new()
local function twistSwing(cf, direction)
	local axis, theta = cf:ToAxisAngle()
	local w, v = math.cos(theta/2),  math.sin(theta/2)*axis
	local proj = v:Dot(direction)*direction
	local twist = CFrame.new(cf.x, cf.y, cf.z, proj.x, proj.y, proj.z, w)
	local swing = twist:Inverse() * cf
	return swing, twist
end

local swing, twist = twistSwing(cfB, line.CFrame.UpVector)

2019-08-27_20-28-19

16 Likes

I dont know what we’d do without you. Another great tutorial which will help me greatly. Thanks for making this! The face cframe will definetely help me a ton.

Sorry for bringing this on a year later, but the getFaceCF seems to ‘double’ the rotation of the part into the face CFrame and I can’t seem to figure out why. Do you have any idea what’s causing this?

2 Likes

Can you specify which function you’re talking about? I can’t find one in my post called getFaceCFrame?

2 Likes

Yeah, the getSurfaceCF function my bad. Seems to almost double the rotation. Ive tried messing with a few variables and couldn’t get anything as I still don’t get whats 100% going on. Im supplying it with the faces normal from a raycast and the part if that’ll help.

OgybnEpm62

Thanks for replying

3 Likes

You should be inputting the object space normal into that function. You can modify the function for world space normals if that’s what you desire:

local function getSurfaceCF(part, wnormal)
	local lnormal = part.CFrame:VectorToObjectSpace(wnormal)
	local transition = getRotationBetween(Vector3.new(0, 1, 0), lnormal, Vector3.new(0,0, 1))
	return part.CFrame * transition * EXTRASPIN
end

Alternatively if you only want surfaces of rectangles you can use a simpler function that doesn’t rely on quaternions.

local function getSurfaceCF(part, enum)
	local v = -Vector3.FromNormalId(enum)
	local u = Vector3.new(v.y, math.abs(v.x + v.z), 0)
	local lcf = CFrame.fromMatrix(Vector3.new(), u:Cross(v), u, v)
	local cf = part.CFrame * CFrame.new(-v * part.Size/2) * lcf

	return cf, Vector3.new(
		math.abs(lcf.XVector:Dot(part.Size)),
		math.abs(lcf.YVector:Dot(part.Size)),
		math.abs(lcf.ZVector:Dot(part.Size))
	)
end
6 Likes

Ah, thanks for the help, thats fixed the issue. Im trying to get on all surfaces because I’m working on a placement system that will snap to a grid based on the surface + center of that surface.

1 Like

I think the second function will work better for you then as it provides the size as well as the cframe for the surface.

The cframe will always line up so that the top left corner is the same as a surface gui on the same face. The size is (Width, Height, Depth) of the part relative to the face.

2 Likes

Alright, I’ll check the second function out later today I’ve been using a bootleg solution for getting the face size.

You’ve helped out a ton with this, so thanks again!

2 Likes

Another useful set of functions for decomposing the angles from different Tait-Bryan angles.

local toEulerAngles = {}

-- see Tait-Bryan angles written in matrix form here:
-- https://en.wikipedia.org/wiki/Euler_angles

function toEulerAngles.XZY(cframe)
	local m = {select(4, cframe:GetComponents())}

	local x = math.atan2(m[8], m[5])
	local z = math.asin(-m[2])
	local y = math.atan2(m[3], m[1])

	return x, y, z
end

function toEulerAngles.XYZ(cframe)
	-- alternatively:
	-- local m = {select(4, cframe:GetComponents())}
	
	-- local x = math.atan2(-m[6], m[9])
	-- local y = math.asin(m[3])
	-- local z = math.atan2(-m[2], m[1])
	
	-- return x, y, z
	
	return cframe:ToEulerAnglesXYZ()
end

function toEulerAngles.YXZ(cframe)
	-- alternatively:
	-- local m = {select(4, cframe:GetComponents())}
	
	-- local y = math.atan2(m[3], m[9])
	-- local x = math.asin(-m[6])
	-- local z = math.atan2(m[4], m[5]);

	-- return x, y, z
	
	return cframe:ToEulerAnglesYXZ()
end

function toEulerAngles.YZX(cframe)
	local m = {select(4, cframe:GetComponents())}

	local y = math.atan2(-m[7], m[1])
	local z = math.asin(m[4])
	local x = math.atan2(-m[6], m[5])

	return x, y, z
end

function toEulerAngles.ZYX(cframe)
	local m = {select(4, cframe:GetComponents())}

	local z = math.atan2(m[4], m[1])
	local y = math.asin(-m[7])
	local x = math.atan2(m[8], m[9])

	return x, y, z
end

function toEulerAngles.ZXY(cframe)
	local m = {select(4, cframe:GetComponents())}

	local z = math.atan2(-m[2], m[5])
	local x = math.asin(m[8])
	local y = math.atan2(-m[7], m[9])

	return x, y, z
end

return toEulerAngles
9 Likes

This is really a great, help, thanks for posting!

1 Like

This was a gigantic help, but I’m experiencing some issues that I don’t exactly know how to solve (not a math person). I’m using this for a 3D proximity prompt system, but the letter seems to get rotated relative to the object, where I want it to always be pointing upwards, so it’s readable.
Here’s my code,

local EXTRASPIN = CFrame.fromEulerAnglesXYZ(math.pi/2, 0, 0)

local function getRotationBetween(u, v, axis)
	local dot, uxv = u:Dot(v), u:Cross(v)
	if (dot < -0.99999) then return CFrame.fromAxisAngle(axis, math.pi) end
	return CFrame.new(0, 0, 0, uxv.x, uxv.y, uxv.z, 1 + dot)
end

local function getSurfaceCF(part, lnormal)
	local transition = getRotationBetween(Vector3.new(0, 1, 0), lnormal, Vector3.new(0,0, 1))
	return part.CFrame * transition * EXTRASPIN
end

local function getPromptCFrame(promptGUI : BasePart, prompt : ProximityPrompt) 
	local destinationCFrame
	local params = RaycastParams.new()
	params.FilterType = Enum.RaycastFilterType.Include
	params.FilterDescendantsInstances = {prompt.Parent}
	local ray = workspace:Raycast(Camera.CFrame.Position,(prompt.Parent.Position - Camera.CFrame.Position).Unit * 100, params)
	if ray then
		destinationCFrame = getSurfaceCF(prompt.Parent,ray.Normal)
		destinationCFrame = destinationCFrame + (prompt.Parent.Size * destinationCFrame.LookVector) / 2
	else
		destinationCFrame = prompt.Parent.CFrame
	end
	return destinationCFrame
end

and a video of what I mean

External Media
1 Like

Hey, i’ve been hunting down a problem in my placement system for a while. Please take a look at it if you have the time.. I think this might be the solution, but when inputting the world normal into that function then it doesn’t work on rotated surfaces?

local function getSurfaceRotationalCF(part, wnormal)
	local EXTRASPIN = CFrame.fromEulerAnglesXYZ(math.pi/2, 0, 0)
	local lnormal = part.CFrame:VectorToObjectSpace(wnormal)
	local transition = getRotationBetween(Vector3.new(0, 1, 0), lnormal, Vector3.new(0,0, 1))
	return transition * EXTRASPIN
end
...
	local _rotation: CFrame
	if relativeRotation then
		_rotation = getSurfaceRotationalCF(target, rayCastResult.normal)-- * EXTRASPIN
	end
	local resolvedCFrame: CFrame = CFrame.new(hitPosition) * _rotation

Animation

Sorry for the bump I just really want to get this stupid problem over with