Rotate CFrame UpVector, loosely preserve LookVector (Change of gravity / Wall running)

I am currently writing a CFrame script for rotating CFrames while preserving the LookVector.
I intend to use this for scenarios where a player’s gravity might change or when a character is tilted.

Ideally in such a scenario you might want to tilt the camera as well without accidentally doing some drastic changes to the LookVector.

If the player were to be inside a rollercoaster, you might also want the camera’s UpVector to align with the carts without sudden jitter or unexpected behavior happening to the LookVector.

My code so far MOSTLY does what it needs to as seen in these screenshots.

RobloxStudioBeta_YMoMnPK6OB

RobloxStudioBeta_QO48eWG2gW

RobloxStudioBeta_Dd75kuIAAu

As you can see, the LookVector is well preserved in most cases, even if the red block is facing an entirely different direction.

This is exactly what I want, however there are some minor issues that arise in specific scenarios.

My video recorder refuses to work right now so I’ll describe the problems instead.

  • I use a form of linear interpolation / stepping to smooth the movement, some orientations however result in VERY slow interpolation or cause the character to get almost stuck temporarily.

  • Under some specific awkward angles, the LookVector will change to a completely undesired one (e.g., character flipping backwards).

The code

  • Vector stepping / interp function (works as intended for simple stuff).
@native function module.step(
	from     : Vector3,
	target   : Vector3,
	stepsize : number

)  : Vector3


	return (from - target).Magnitude > stepsize

		and from + (target - from).Unit * stepsize
		or target
end




return module
  • CFrame module.
local module = {}


local vmod = require(script.Parent.vector)

local vstep = vmod.step




@native function module.step(
	from        : CFrame,
	target      : CFrame,
	
	stepsize    : number,
	rotstepsize : number

)  : CFrame
	
	
	return CFrame.lookAlong(
		vstep(from.Position,   target.Position,   stepsize)
		,
		vstep(from.LookVector, target.LookVector, rotstepsize)--.Unit
		,
		vstep(from.UpVector,   target.UpVector,   rotstepsize)--.Unit
	)

end

local step = module.step



-- Rotates a CFrame while trying to preserve LookVector as much as possible.

@native function module.step_rot_up(
	from        : CFrame,
	target      : CFrame,

	stepsize    : number,
	rotstepsize : number

)  : CFrame


	local stepped : CFrame = step(from, target, stepsize, rotstepsize)
	
	local look    : CFrame = stepped.UpVector:Cross(from.RightVector)


	return CFrame.lookAlong(stepped.Position, look, stepped.UpVector)

end








return module
  • Script that rotates the Dummy’s UpVector towards the UpVector of the red brick.
    (Placed inside it’s HumanoidRootPart.)
local mod = require(game.ReplicatedStorage.glib.common.math.cframe)


while task.wait(.01) do
	
	local pos = script.Parent.CFrame
	
	local target = workspace.code_test.partb.CFrame
	
	script.Parent.CFrame = mod.step_rot_up(pos, target, 0, .05)
	
end

Don’t mind the fact that I’m not using RunService or anything, this is just some test code to see if the CFrame math works out.
I will see if I can provide recordings later to show the problem with more detail.

I’m quite confused what your problem is here. I’m mostly stuck because the UpVector will always be the same relative to the LookVector. If you could explain a bit more about what exactly you’re trying to do, I might be able to help :sob:

Not sure if the video will load bit it shows the problem.
At first the character will rotate with their feet pointing towards the bottom of the red box.

In the last few seconds, when I rotate the character using the BLUE axis, it starts goofing up and making awkward spins.

I don’t understand your problem. What is your use case for this? I’m going to have to take a guess.

That shouldn’t be a problem if you’re using CFrame:Lerp() and CFrame.lookAlong. I see in your code that you are using lookAlong.

Instead of CFrame:Lerp you are using a linear vector lerp. This comes with problems if you’re dealing with rotation-like behavior since linear interpolation might not always take the best path. I recommend looking into Spherical Lerping (slerp).

function module:SphericalLerp(a: CFrame, b: CFrame, percent: number)
	local relative = a:ToObjectSpace(b)
	local axis, angle = relative:ToAxisAngle()
	local newAngle = angle * percent
	local interpolated = CFrame.fromAxisAngle(axis, newAngle)

	return a * interpolated
end

function module:ContinuousSphericalLerp(a: CFrame, b: CFrame, percent: number, step: number)
	local newPercent = module:DiscretizeStep(percent, step)
	return module:SphericalLerp(a, b, newPercent)
end

I attached slerp code I’ve written from another post if you want to try it.
Let me know if there’s any issues!

I don’t use the built-in Lerp functions because I might want to implement something like Camera smoothing / add lag to the rotation so it’s not hard-locked to the player character orientation.

The rollercoaster thing is an example, but I’m basically working on some kind of custom physics engine that would allow a character to walk onto a wall or ceiling and rotate the camera along with it.

Standard Lerp poses problems since it’s “speed” depends on the range between point A and point B when I want the interpolation to be constant regardless of distance between points.

So I wrote that simple step-based Lerp to do that but I’m probably still doing things incorrectly.

My approach was to break a CFrame into it’s Position, UpVector and LookVector component and interpolate it to it’s NEW CFrame while preserving the LookVector and not worry about the RightVector since you can just CrossProduct() it back into existence.

But the use case for all this is basically… Well, if you know the game Mario Galaxy, you can probably tell what I’m getting at.
I want to perfect and polish rotations to achieve that smooth transition between changing gravity and camera positions.

The gravity comes later though, I figured I do rotation first since that arguably seems like the hardest part and is also needed for the camera in general for things like tilting.

Ok, I see.

I think the best solution would be to rely on CFrames. They preserve that rotation + position information with a mathematical foundation for interpolated rotation (normal vectors suffer gimbal lock while CFrames, which use quaternions, do not).

Have you tried just storing your camera as a CFrame instead? You can set a new target lerping position using lookAlong. Something like:

local rawCFrame = workspace.CurrentCamera.CFrame

while true do
	task.wait()
	local rawCFrame = rawCFrame:Lerp(
		CFrame.lookAlong(newCamPosition, character.PrimaryPart.CFrame.LookVector, surfaceNormal) -- surfaceNormal is obtained from raycasting down at the players feet to get the surface they're standing on
	, amount)
	
	if math.random(1,100) == 1 then -- you can layer other effects on top
		workspace.CurrentCamera.CFrame = rawCFrame * CFrame.Angles(0,math.random(),0)
	else
		workspace.CurrentCamera.CFrame = workspace.CurrentCamera.CFrame:Lerp(rawCFrame, 0.1)
	end
end

The only challenge is obtaining the LookVector since it has to auto update every time the surface normal changes, but I assume you already have that from your wall climbing engine.

If you want the RootPart to face where the red part faces, you can also use AlignOrientation because it does the same thing but uses Attachment

I could be wrong, but a possibility could be that once the rotation of an axis reaches 180 degrees, it changes to 0 degrees.

1 Like

If it hasn’t been mentioned before, you could try flattening the CFrame first before passing it into the smoothening part. This way you’d ensure any other axis wouldn’t impact the target axis

I doubt this is the solution you are looking for though :pensive:

1 Like

This could’ve worked in a physics-based approach, however I intend to do everything with raycast and explicit CFraming to get precise control over character movement and prevent Roblox jank from being able to make a character trip or get flinged.

CFrame.lookAlong(newCamPosition, character.PrimaryPart.CFrame.LookVector, surfaceNormal)

This line right here, this was actually my first attempt at getting a character to rotate.

But this caused the problem that the LookVector was ALWAYS preserved, resulting in some rotations becoming downright impossible.

Oh my goodness, why did I NOT think of that?
I will actually try your solution here, maybe flattening the LookVector actually does it, I just have to remember what the math for that was again.

I think some silly ObjectSpace/WorldSpace conversion MIIIIGHT do the trick.
I’ll update if I made progress.

1 Like

Could you clarify what you mean by this?

I don’t understand this either. You mention here that you want to preserve the LookVector, but your previous response was that you didn’t.

Based on what I see. If you want smooth rotations between two different surfaces when you walk between them, use Slerp. It always finds the shortest path on a sphere when traveling between two distances. I provided the CFrame code for it.

Slerping should handle your character transitions. To obtain the CFrames for each surface, you just do CFrame.lookat(pos, pos + blackArrow, blueArrow).

That should be enough to get a nice rotating camera.

hi, cant you just set the target cframe to the camera cframe * cframe.angles(1, 0, 1)?

Sorry, I should clarify, I wish to preserve the LookVector as much as possible UNLESS it would prevent the player from walking straight/directly up a wall.

Basically, the LookVector would need to be preserved if you were to walk onto a wall sideways, but not if you walk onto the wall facing forwards/backwards.

Ideally all of this would happen inside a single CFrame function so I don’t have to do a whole bunch of if-else-if-else statements to check if a character is facing the wall perpendicularly or sideways.

Forget about smoothing/interpolation for a second here, let’s say if I were to snap a player directly onto a wall, their UpVector needs to match the wall’s surface Normal.

But the character needs to be facing the direction they were originally walking into, even if side-ways (to make shift-lock possible while walking up against a ramp onto the wall).

I ALMOST did it, except it now only does the rotation forwards.

Huh, I thiiiink it might be possible if I rotate the character one axis at a time?
Maybe I need a more sequential approach.

Ohh, I understand now. I assume you want all degrees of rotation (XZ + XY + YZ) to be tracked instead of just horizontal (XZ) rotation.

What I suggest is to store 2 CFrames: one is the player’s CFrame (playerCF), and one is the true CFrame (trueCF).

Key idea: player rotation is relative based on the surface you’re standing on. We apply that same relative rotation to a new surface when we walk on it.

In this picture, trueCF is marked with black arrows. Surface normal is marked with blue. Player cf is not included.


trueCF is determined by your current position minus your previous position with the current surface normal as UpVector.

trueCF = CFrame.new(currentPos, currentPos + (currentPos - lastPos), surfaceNormal)

playerCF is just the root part’s CFrame

playerCF = character.PrimaryPart.CFrame

You can derive deltaCF now. This lets you turn any trueCF values into the current player rotations. You can focus on doing the wall climbing algorithm using trueCF, then apply the deltaCF rotation to preserve the original player orientation relative to trueCF before the climb switches.

-- derivation of deltaCF
playerCF = trueCF * deltaCF -- defined formula
trueCF:inverse() * playerCF = trueCF:inverse() * trueCF * deltaCF -- multiple both sides by trueCF inverse from the left
trueCF:inverse() * playerCF = deltaCF -- trueCF:inverse() * trueCF is an identity CFrame, so it cancels out

deltaCF = trueCF:inverse() * playerCF -- final derivation
deltaCF = deltaCF - deltaCF.Position -- get rid of the position component

On wall switches, you can simply compute a new trueCF and multiply it with deltaCF to preserve your original orientations.

function getCurrentDelta(trueCF: CFrame, playerCF: CFrame)
	-- run this right before you change surface normals
	deltaCF = trueCF:inverse() * playerCF
	deltaCF = deltaCF - deltaCF.Position
	
	return deltaCF
end

local lastSurfaceNormal
while true do
	task.wait()
	trueCF = CFrame.new(..., surfaceNormal) -- assume you've already derived this from your wall climber algorithm
	playerCF = character.PrimaryPart.CFrame
	deltaCF = getCurrentDelta(trueCF, playerCF)
	
	if newSurface then
		-- you find detect a new surfaceNormal, readjusting the player...
		local newTrueCF = CFrame.new(..., newSurfaceNormal) -- set the new direction orientation
		local newPlayerCF = newTrueCF * deltaCF -- from that direction orientation, apply the relative rotation of the player
		
		character:PivotTo(newPlayerCF)
	end
end

If you use CFrame:Lerp, you will also get smooth spherical rotations instead of the strange path in your video.

1 Like

I believe this is the function you’re looking for:

local function fromToRotation(fromUnit: Vector3, toUnit: Vector3, backupAxis: Vector3)
	local dot, cross = fromUnit:Dot(toUnit), fromUnit:Cross(toUnit)
	if dot < -0.99999 then
		return CFrame.fromAxisAngle(backupAxis, math.pi)
	end
	return CFrame.new(0, 0, 0, cross.X, cross.Y, cross.Z, 1 + dot)
end

You can read about it here.

It seems like you’re playing around with the idea of walking on walls or something. I recommend looking into my profile as I have a bunch of resources (albeit quite old and not up to my current standards) related to that concept.

-- pseudocode
UserInputService.InputBegan:Connect(function(input, sink)
	if input.UserInputType == Enum.UserInputType.MouseButton1 then
		local rayResult = mouseRaycast()
		
		if rayResult then
			local floorOffset = CFrame.new(0, humanoid.HipHeight + rootPart.Size.Y / 2, 0)
			
			local prevRootCF = rootPart.CFrame
			local arc = fromToRotation(prevRootCF.YVector, rayResult.Normal, prevRootCF.XVector)
			
			rootPart.CFrame = CFrame.new(rayResult.Position) * (arc * prevRootCF).Rotation * floorOffset
		end		
	end
end)
4 Likes

It’s it possible that you’re way over-complicating this? It seems to me that all you really want is to get the rotational different between the UpVector directions and apply it extrinsically to the character’s orientation. Run this example file I just made and see if this is doing what you were expecting:

rotationexample.jpg
RotationExample.rbxl (90.1 KB)

This place expect you to just hit Run (F8) and manipulate the cube with the #4 rotation tool.

All this is doing is getting the CFrame required to rotate the cube’s UpVector from it’s previous to current direction, and then slap it on the character. If you want to smoothly interpolate this, do it by interpolating the angle in axis-angle space, don’t linearly interpolate the direction vector’s like you’re doing; that will have weird acceleration for large angles and also has a singularity at 180 where you’ll get a zero-vector different (causing Vector3.Unit to error with NaN).

2 Likes

Yeah, this is basically what I am attempting to do, I’m trying to understand your code however.

local function fromToRotation(fromUnit: Vector3, toUnit: Vector3, backupAxis: Vector3)

	local dot, cross = fromUnit:Dot(toUnit), fromUnit:Cross(toUnit)

	if dot < -0.99999 then
		return CFrame.fromAxisAngle(backupAxis, math.pi)
	end

	return CFrame.new(0, 0, 0, cross.X, cross.Y, cross.Z, 1 + dot)
end

Attempting to visualize it in my mind and also assuming that backupAxis would be the character’s LookVector??

I see you’re writing directly to a CFrame’s matrix here, of which I have a basic understanding but I’m still trying to grasp why this method would work best.

return CFrame.new(0, 0, 0, cross.X, cross.Y, cross.Z, 1 + dot)

has 7 arguments in total, with the first 3 being a position, and the other 4 being a quaternion or angles with a W axis added?

If you could explain the steps taken here I’d appreciate that a ton!
Some of your math blows my mind and I keep wondering how you come up with such ideas to do things a certain way when my methods often just involve “brute forcing CFrames until it looks right”.

The explanation / derivation for that function is outlined in the post I linked in my first reply. The name of the function is different in the post (getRotationBetween), but it’s the same code.

A few notes on your own conclusions:

  • The backup axis defines what to rotate around in the event that there’s 180 flip. You could switch it to the look vector, but it depends what feels right in your own context.
  • The CFrame constructor with 7 arguments is position + quaternion rotation of the form: CFrame.new(x, y, z, qx, qy, qz, qw)
  • I did not invent this :laughing:. This is a well defined operation that’s commonly included in other engines (i.e. unity). All I’ve done is explain it and write it in luau in that post.

Also worth mentioning that Emily’s solution is very similar to mine so if that code is easier to understand then read / use that!

1 Like