Cframe.LookAt "Up" value \ ant-like wall crawling

For future reference, the character is a single part suspended in zero gravity. There is no humanoid.

I’m attempting to make a system that allows walking up walls like an ant, but in attempting to create this I’ve found a pretty tricky issue. The normal of the raycast, when turned into a cframe with Cframe.LookAt(), returns some additional rotation that I dont want.The Up value seems to be the answer to fixing this, but I’m unsure what to set it to.

image
(fnorm is the current floor normal, which lerps continuously to the normal of the raycast hitting the floor. In this screenshot I’ve set the Up value to the inverse of fnorm)


you can see how my camera seems to turn towards random directions, and in cases that invert vertically they flip the character around 180.

This is what I have currently, it works for play-testing, but it still causes the player to turn in random directions or flip occasionally. This won’t be viable for release.

I would appreciate either an explination of how the Up value is used, or another cframe method that achieves what I’m looking for.

Please don’t forward me to egomoose’s wallstick/gravity controller, its old, hard to read, build for humanoid code, and gigantic.

6 Likes

When you rotate a CFrame 90 degrees it causes 2 of the x, y, or z axes to align with each other due to gimbal lock. This causes the effect similar to what you described. Picture it like the Studio Rotate tool’s handles, but when you rotate the X handle 90 degrees the other 2 stay in the same orientation which means your X and Y axes are now the same.

Try searching ‘gimbal lock’ using the search too. There are a few posts about it, including ways to keep it from happening. They explain the Solution much better than I can.

1 Like

I wouldn’t say all cases are gimbal lock, I’ve provided a video of what the issue looks like. It rotates left and right when it should be just tilting to fit the new floor.

I’m thinking the rotations are so extreme that they could cause gimbal lock, that could be a possibility, but not the cause by any means

3 Likes

It may be because the MeshPart you are climbing is physically a different shape than it is rendered. This can cause tri faces to be pointing in slightly different directions than you’d expect causing the Normal direction the raycast is reading to be off a bit.

Maybe try going into your File > Studio settings > Studio > Visualization > Show Decomposition Geometry. You may have to select the MeshPart you are climbing and change a Property to get it to show the recolored version that displays how the mesh is really shaped.

If that’s the case you could try clicking on the MeshPart and changing its CollisionFidelity Property from Default to PreciseConvexDecomposition. This will cause the MeshPart to be shaped closer to its rendered shape.

3 Likes

Definitely isn’t the mesh, its a torus ported straight from blender

1 Like

Yes, but just because a mesh is directly imported from Blender doesn’t mean that Studio automatically sets the CollisionFidelity Property to PreciseDecompositionGeometry. It’s set to Default, which isn’t always the physically smooth surface it appears to be.

I guess another question is does this happen on flat Parts that are tilted?

2 Likes

Have you tried offsetting the cframe by CFrame.Angles() instead of straight up rotating it so what I mean is like:

part.CFrame = part.CFrame * CFrame.Angles() -- retains the position and rotates correctly

Instead of this:

part.CFrame = CFrame.Angles() -- the part will move and rotate strangely.
2 Likes

that would cause the character to violently spin, because im adding rotation to a rotated part

1 Like

it doesn’t matter what a mesh looks like, normals don’t have rotation they’re unit vectors. Its not the mesh

1 Like

Maybe try testing it, I’m not the best at cframes but who knows it might work.

2 Likes

You can get the angle between 2 vectors using Vector3:Angle() if I recall.
Then use Vector3:Cross() to get the axis on which you have to rotate the character.

In both you input the current normal the player is standing on and the normal the player needs to rotate to.

I suppose you could do a simple CFrame1 * CFrame2 with rotation here to achieve the result.

1 Like

The “Up” component (3rd parameter) of lookAt specifies the ground plane for orientating the character.

There are infinite solutions if you wanted set the CFrame of a part/model to face a position.
Both of these are valid solution.


The ambiguity is removed when the ground plane is specified. There is now only 1 solution to have the player face the position.

Attached is an example of not specifying lookAt versus using it.
[LEFT]: not using the 3rd parameter at all (defaults to 0, 1, 0)
[RIGHT]: setting the 3rd parameter to the surface of the normal the target is standing on.


Back before this was a thing you were stuck with the option on the left. So if you wanted to make a character that walks on walls you’ll notice the character might be facing the wrong direction.

Conclusion: Set the 3rd “Up” value to the normal of the surface you’re walking on since it’s the ground plane for orientating the character.

Your wording here is very ambiguous. I’m not sure what “turn in random directions or flip occasionally” means. I think everyone here is assuming that you don’t like how the camera is “snapping” to certain orientations as you’re walking along the surface.

What Scottifly said explains the situation well. Unless you’re representing your surface mathematically, you cannot get smooth camera rotations as you move along it. Roblox’s raycast relies on the hitbox generated for the mesh, the generated hitbox is always an approximation using multiple planes.

Couple of ideas:

  1. Use terrain instead. You can replace the materials/texture of the terrain if you don’t like how it looks. I’ve noticed that you can get more precise surface approximations than imported meshes.

  2. Compute your own approximations. You can define the surface geometry of each mesh as a mathematical formula and query the surface normal on it. Another option is that you can also sample the nearby planes and construct a mathematical smooth surface, similar to a bezier curve, but as a plane instead.

  3. Use more parts for curves and live with it. This is a hard problem to solve on Roblox. If you look at EgoMoose’s demo, you’ll see that slight camera snapping present whenever you walk up and down.

I’ve made my own wall climbers before. This is as good as it gets without going into the more complicated math. The best solution for me was just using more parts to get a smoother curve.

Good luck!

5 Likes

I might have an idea on how to achieve slightly smoother curves without using practically infinite parts.
I got the idea from image processing and anti-aliasing algorithms.

What one could do to achieve slightly smoother curves is doing a bunch of raycasts all around the character and then smoothly interpolating to some average result instead of directly snapping to the surface.

Of course not a perfect method but might help with reducing sudden snapping at least and is not too complex to implement.

1 Like


this is a good example of the rotation im talking about

this is the code for that, im not sure what ive done wrong and ive had the up set to the normal this whole time.

I think i could fix this by having it look at the lookieXY with the norm as the up, but i’d have to reformat it a bunch for that to work.

1 Like

Does this work the intended way? This rotates the character in the plane that is parallel to both the previous surface normal (char.CFrame.UpVector) and newSurfaceNormal. The axis vector is a unit normal vector of this plane (the rotation happens around axis) and angle is the angle between the previous surface normal and newSurfaceNormal.

local equalityThreshold: number = .01

local function areApproximatelyEqual(a: number, b: number): boolean
	return math.abs(b - a) <= equalityThreshold
end

local function getAxisAngleCFrame(originalVector: Vector3, rotationTargetVector: Vector3): CFrame
	if areApproximatelyEqual(originalVector.Magnitude, 0) or areApproximatelyEqual(rotationTargetVector.Magnitude, 0) then
		error("At least one of the given vectors is approximately a zero vector.")
	end
	originalVector, rotationTargetVector = originalVector.Unit, rotationTargetVector.Unit
	local crossProduct: Vector3 = originalVector:Cross(rotationTargetVector)
	local dotProduct: number = originalVector:Dot(rotationTargetVector)
	if areApproximatelyEqual(crossProduct.Magnitude, 0) then
		-- In this case, the dot product is close to either 1 or -1.
		return if dotProduct > 0 then CFrame.identity else CFrame.fromMatrix(Vector3.zero, Vector3.xAxis, -Vector3.yAxis)
	end
	local axis: Vector3 = crossProduct.Unit
	local angle: number = math.acos(dotProduct)
	return CFrame.fromAxisAngle(axis, angle)
end

local function updateCharCFrame: (char: BasePart, newSurfaceNormal: Vector3): ()
	char.CFrame = getAxisAngleCFrame(char.CFrame.UpVector, newSurfaceNormal) * char.CFrame.Rotation + char.Position
end

Edit: I had accidentally written “perpendicular to both” instead of “parallel to both”.

5 Likes

Thanks a ton, it jitters a bit as of now but it does indeed work.

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