Help with aligning mob to the ground

Hello! I am currently working on a pet following system in which the pet matches the Y orientation of the player while also being properly planted on the ground. Here’s a visual example:

https://gyazo.com/3989cc266a0d553e9617c1840f75cd0e

As you can see, the pet rotates along with the player. This works just fine on a flat plane, but not so much on a slope. Here’s what it should look like:

Here’s how it is: https://gyazo.com/6937fe0c32891f5c7c06134f1b92cd98

The pet rotates incorrectly (in a different direction from the player) depending on the normal of the surface. This is because (I assume) the normal’s UpVector is influencing the overall rotation of the pet.

Here’s the code I have so far:

-- result is from a raycast pointing to the ground
local position = result.Position
local plrLookVector = charHrp.CFrame.LookVector

-- these angles are added to the equation to keep the pet upright
local calibrateAngle = CFrame.Angles(math.rad(-90), math.rad(180), 0) -- Used to correct look & normal vectors
local offsetAngle = CFrame.Angles(math.rad(-90), math.rad(-90), 0) -- Used to correct resulting vector

-- lookVector and normals are converted to CFrames to help visualize the results
local lookCf = CFrame.lookAt(position, position + plrLookVector) * calibrateAngle
local normalCf = CFrame.lookAt(position, position + result.Normal) * calibrateAngle

-- This is where the problem lies
local finalCf = CFrame.new(position) * (normalCf - normalCf.Position) * (lookCf - lookCf.Position) * offsetAngle

-- set values for BodyGyro and BodyPosition inside pet rootpart
info.BodyGyro.CFrame = finalCf
info.BodyPos.Position = position

So the question is, how can I combine the X and Z rotation from normalCf with the Y rotation from lookCf without them clashing? Am I going about this the right way? Vector math is new to me, so I’m not quite sure where to start.

Any help is appreciated. Thanks in advance!

The easiest solution to this problem that I could think of was to use two Cross products. What I mean by that is that we take the cross product of the player’s -LookVector and the Surface normal to get the RightVector, since the RightVector remains constant regardless of the rotation of the pet. And because the surface normal is the same as the UpVector, and all we need for rotation is two vectors, the forward vector is just the negative cross product between the up vector and right vector.

Hopefully that makes sense, it assumes you know a little bit about vector math and cross products, and if not I recommend looking at some videos on it. In summary, the cross product of two vectors is a vector perpendicular to both of the supplied vectors. While this can be negative or positive depending on which direction you want, the equation for said line in 3d space is constant and linear.

It also assumes you know about how CFrame’s work, and how it is a matrix of a positional vector and 3 directional (unit) vectors. This allows us to compute the following:

-- result is from a raycast pointing to the ground
local position = result.Position
local plrLookVector = charHrp.CFrame.LookVector

-- This is where the solution lies
local up = result.Normal
local right = up:Cross(-plrLookVector).unit
local forward = -up:Cross(right).unit
	
local finalCf = CFrame.fromMatrix(position, right, up, forward)
	
-- set values for BodyGyro and BodyPosition inside pet rootpart
info.BodyGyro.CFrame = finalCf
info.BodyPos.Position = position

Side note: I have no idea what you’re doing with the “lookCf”, “normalCf” and calibrate/offset angles. You might have to add these back or recompute them if your pet is (for whatever reason) not based on standard space, though I wouldn’t say Roblox is “standard” either considering Y is up instead of Z but meh whatever. My pet “Bricky” worked pretty well by just setting its CFrame to the finalCF multiplied by CFrame.new(0,1,0) (to account for height):

Screenshot_5
Screenshot_4

2 Likes

Thank you so much! My solution had been to use CFrame.lookAt with the plrLookVector as the lookAt and the normal as the UpVector, but it wasn’t always accurate. Your solution works perfectly.

Quite honestly, I don’t know either. Starting off on the wrong foot can really take you down a strange road. I definitely have some homework to do when it comes to understanding all of this, specifically why the look- and up-vectors need to be flipped while getting the cross products. But, a bit of trial and error can teach me about that stuff. Thanks again.

3 Likes