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:
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:
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.
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):
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.