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.
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
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.
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.
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)
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.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).
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
. 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!
I am reviewing @EmilyBendsSpace’s code here.
It does seem to do the exact thing that I am trying to achieve, I might mark it as the solution if it ends up working.
(Some comments added by me to express thought.)
local part = game.Workspace:WaitForChild("Block")
local dummy = game.Workspace:WaitForChild("Dummy")
local oldUp = part.CFrame.UpVector
local EPSILON = 1e-6 -- Tolerance value?
function GetAxisAngleToRotateVectorAtoB( a, b )
if a.Magnitude < EPSILON or b.Magnitude < EPSILON then
return Vector3.yAxis, 0
end
local ua = a.Unit
local ub = b.Unit
local dp = ua:Dot(ub.Unit) -- I wonder what we're using Dot product for.
local cp = ua:Cross(ub) -- Cross between Unit A and B, likely the axis we want to rotate on?
if math.abs(dp) > (1.0-EPSILON) or cp.Magnitude < EPSILON then -- Dot product used as some tolerance value?
if dp < 0 then
return Vector3.yAxis, math.pi -- Pi is used here?
end
return Vector3.yAxis, 0
end
local axis = cp.Unit
local angle = math.atan2( cp.Magnitude, dp ) -- What does arc tangent do with cross and dot?
return axis, angle
end
part:GetPropertyChangedSignal("CFrame"):Connect(function()
local newUp = part.CFrame.UpVector
local axis, angle = GetAxisAngleToRotateVectorAtoB(oldUp, newUp)
local rotCF = CFrame.fromAxisAngle(axis,angle)
local dummyCF = dummy:GetPrimaryPartCFrame()
-- Why subtract position if we add position back after??
dummyCF = rotCF * (dummyCF - dummyCF.Position) + dummyCF.Position
dummy:SetPrimaryPartCFrame(dummyCF)
oldUp = newUp
end)
I understand most of this code, the one part that kiiiinda confuses me a bit are these?
if a.Magnitude < EPSILON or b.Magnitude < EPSILON then
return Vector3.yAxis, 0
end
My guess is that this is some sort of check to basically prevent “division by 0” sorta scenarios but for rotation?
Or it’s to rotate a character 180 degrees onto a ceiling?
I just see Magnitude < Epsilon
and the first thing coming to mind is that if a value is super tiny or super large we might want to do something different with it.
if math.abs(dp) > (1.0-EPSILON) or cp.Magnitude < EPSILON then
if dp < 0 then
return Vector3.yAxis, math.pi
end
return Vector3.yAxis, 0
end
Here I see Pi
being returned as the angle of the function.
Why Pi
specifically?
local angle = math.atan2( cp.Magnitude, dp )
Arc tangent! I honestly forgot what it exactly does, I’ve only used a tangent function before when writing a neural network activation function to remap a input value (that was hyperbolic tangent).
I haven’t touched trigonometry in ages, haven’t fully learned it.
If the point of Devforum is learning then I am very curious about these specific choices made in code.
The premuliplication by rotCF
should only affect the orientation of the dummy, not rotate his world space location about the world origin, so it’s applied only to the rotation part of the CFrame. The unmodified position of the part is then restored by adding it back. The alternative way to do this would be to conjugate the rotation change into dummy local space, i.e. replace that line of code with:
dummyCF = dummyCF * (dummyCF-dummyCF.p):Inverse() * rotCF * (dummyCF-dummyCF.p)
Either way, it’s basically subtracting and re-adding the dummy’s world space position, effectively moving the dummy to the origin, re-orienting him, then moving back to where he was.
This check is to avoid working with tiny vectors. In practice, the code I have that uses this helper function is passing in unit vector directions, so this is really just a sanity check specific to my use of this code. I just copy-pasted this function from some actual game code because it was convenient to illustrate the point; it’s doing more checks than you probably need/want in your use.
This bit is just saying the the two vectors are pointing in the same exact direction (or exactly opposite directions), within some tiny tolerance to account for what could just be floating-point difference in how they were calculated, treat them as the same vector and return zero rotation (or perfect 180, respectively). The axis being yAxis is arbitrary but valid when rotation amount is zero, because axis doesn’t matter when there’s no rotation at all.
The code above is actually invalid (incorrect) if the rotation is 180 deg (which is math.pi in Radians), because in that case not every axis is valid, only axes in the plane perpendicular to the vectors a and b, but in the code I copy-pasted this function from, angle of 180 is actually handled as a special case, externally to this function. I have another helper function GetArbitraryUnitPerpendicular( v )
that computes a valid axis of rotation before a CFrame is constructed. So in the sample place I posted above, if you were somehow able to rotate the cube exactly 180 degrees in a single frame, the dummy orientation would end up wrong, because I didn’t include the code that handles that. But that would take some skills!
I accidentally ended up solving the problem after finishing this code here.
Tbh this is perhaps the simple / naive way of doing it now that I think of it.
No complex math functions are used here, it’s all silly CFrame operations chained together.
-- Rotates a CFrame while trying to preserve LookVector as much as possible.
function module.step_rot_up(
from : CFrame, -- Object CFrame
topos : Vector3, -- Target floor / gravity direction.
rotstepsize : number
) : CFrame
-- New normal.
local newup = -CFrame.lookAt(from.Position, topos).LookVector
-- Rotate side-ways 90 degrees.
local newcf : CFrame = from * CFrame.Angles(0,math.rad(90),0)
-- Now CFrame rotates to target on X axis only, then rotate 90 degrees back.
newcf = CFrame.lookAlong(newcf.Position, newcf.LookVector, newup) * CFrame.Angles(0,math.rad(-90),0)
-- Rotate CFrame towards target again, but now on Z axis.
newcf = CFrame.lookAlong(newcf.Position, newcf.LookVector, newup)
-- step() is an optional function that just interpolates CFrames linearly.
return step(from, newcf, 0, rotstepsize) -- (Old, New, Position step, Rotation step)
end
It does something VERY similar to what your code does, except it only uses CFrame instructions in a very silly but non-verbose way I think.
I am still going to compare whether this or your method achieves the best results but also test performance because if I can get player gravity working I might want the same thing for NPCs / AI characters as well.
I can currently not record videos as that doesn’t work on my laptop for some reason but oh me goodness it works so well!
To @EgoMoose @EmilyBendsSpace and everyone else here I am very grateful for all the help I have gotten.
When this gravity engine is functional and production-ready I hope I can eventually release it to the public and open-source it along with some libraries I am working on with (hopefully) optimized and clean code.
I remember some other people on Roblox have either created games or written modules for enabling gravity changes in games but unfortunately some of these libraries and projects are outdated and/or unmaintained.
I aim to create something that is easy to maintain and won’t break due time.
My next step is actually implementing gravity/momentum with raycasts and potentially figure out if I can let a player stand/walk on a moving object but for that I’ll just make a new post if I run into problems.
local epsilon : number = 1e-6 -- Tolerance value (e-6 adds 0 moves the 1 6 places behind the decimal point).
local module = {}
function module.get_axis_for_rotation( a : Vector3, b : vector3 ) : Vector3 & number
local unit_a : Vector3 = a.Unit -- Probably already is a Unit vector.
local unit_b : Vector3 = b.Unit
local dot : number = unit_a:Dot (a) -- Tolerance check?
local cross : Vector3 = unit_a:Cross(b) -- Axis we want.
-- Check if vector isn't 0 or 180.
if math.abs(dot) > (1.0 - epsilon)
or cross.Magnitude < epsilon
or dot < 0
then
if dot < 0 then return Vector3.yAxis, math.pi end
return Vector3.yAxis, 0
end
-- Use CFrame.fromAxisAngle(axis, angle) to get a usable CFrame.
return
cross.Unit, math.atan2( cross.Magnitude, dot )
end
return module
I have reformatted/cleaned up the other function a bit and put it into it’s own module along with some annotations as reminders for why something is there.
Could probably be cleaned up further if I just were to return an Axis Angles right away instead of returning 2 results.
You’ll eventually come up against a specific limitation of some of these helper functions, along with the CFrame.new( fromPos, toPos ) constructor (which is effectively position + lookAt). The limitation is that the arguments to the functions don’t fully specify a unique CFrame, they give you one possible solution from infinitely many. In a normal, upright world, Roblox’s choice of internally taking cross products with the world Y axis usually gives the desired result. But when you go off the rails and upside-down, you may find that you need to use the more general CFrame.fromMatrix(), with all of its arguments fully specified by you (i.e. you provide your own “up” vector)
I figured I would eventually run into something like that (I haven’t yet though).
So far the method I’ve used works fine, it even works for 180 degree rotations which I didn’t even write a sanity check for yet which is rather funny.
I’ve placed the dummy in various different positions and places and found no oddities yet, I’m honestly not even sure what sort of oddities to expect.
There’s probably a reason all the math-nerds define custom functions instead of doing everything with helper functions.
For my specific usecase of simply rotating a object or character while loosely preserving LookVector it seems to do exactly what I wanted it to.
I do kind of entertain the idea of making a custom humanoid / character physics engine that has intentional (or unintentional but still cool) bugs that a player could abuse in speedruns or to perform neat tricks.
I don’t think I have to worry about players having unfair advantages since I mostly focus on non-competitive / single-player type games with perhaps social features just to make it less lonely.
Is there a specific limitation you ran into before though? I’m curious about hearing that.