How can I set a CFrame's angle on one global axis?

I have code to tilt the player’s character with the terrain beneath them so that they align with terrain’s slopes. Now I want to give the player the ability to rotate their character left/right using their controls. However, multiplying a CFrame by CFrame.Angles rotates them relative to their previous orientation, and I don’t want that. I want to control the exact rotation they’re aiming for, not rotate them incrementally. I know how to get the CFrame to represent the player’s left-right controls, but I can’t figure out how to combine this CFrame with the CFrame that aligns them with the terrain in order to allow them to turn their character left or right while aligning with terrain

This is (I think) what I need to achieve that:

local TerrainAlignment = CFrame.fromMatrix(Shared.PrimaryPart.CFrame.Position, alignRightVector, alignLookVector:Cross(alignRightVector), -alignLookVector)
TerrainTiltGyro.CFrame = TerrainAlignment
local CameraLookAt =  CFrame.lookAt(Shared.PrimaryPart.Position, Shared.PrimaryPart.Position + game.Workspace.CurrentCamera.CFrame.LookVector)
-- I want to combine the TerrainAlignment CFrame with the global Y orientation of
-- the LookAt CFrame so that the character can rotate left-right to align with the
-- camera on the global Y axis while aligning with the terrain on all other axes

I think this code shows what I’m trying to do, but it doesn’t work; there’s so many different functions for CFrame:toWhateverAngles and CFrame.fromWhateverAngles, I’m not sure what translates to what

local TerrainAlignment = CFrame.fromMatrix(Shared.PrimaryPart.CFrame.Position, alignRightVector, alignLookVector:Cross(alignRightVector), -alignLookVector)
local x, y, z = TerrainAlignment:ToOrientation()
local x2, y2, z2 = CFrame.lookAt(Shared.PrimaryPart.Position, Shared.PrimaryPart.Position + game.Workspace.CurrentCamera.CFrame.LookVector):ToOrientation()
TerrainAlignment = CFrame.new() * CFrame.Angles(x, y2, z)

I think another solution I could do is get the difference between the TerrainAlignment’s orientation on the Y global axis with the orientation on the Y global axis of the LookVector’s CFrame and then increment the TerrainAlignment’s CFrame by the difference, but I’m not sure how to get those values

Something else I tried to do is use different instances to control orientation on different axes, but try as I might I couldn’t figure it out. BodyGyro has the ability to limit torque on specific axes which helps with that, but I’d like to not have to rely on BodyGyro given that it’s deprecated

Thank you very much for any feedback

Hey there,
So what I did to make the player align to slopes was this:

local moveDirection = humanoid.MoveDirection

local surfaceRight = moveDirection:Cross(normal) 
--[[^^^ We're using moveDirection instead of LookVector because
we want the player to rotate in the direction they're moving, instead of being in a fixed look direction]]

local surfaceAlignment = CFrame.fromMatrix(position, surfaceRight, normal)
alignGyro.CFrame = surfaceAlignment -- Or you can use AlignOrientation, whichever one you prefer

Now what sucks is that you have to make your custom movement for WASD/Joystick and jumping since the player will slightly sink into the ground, but easiest way is to just use a BodyVelocity or any body mover and change it’s velocity/direction to the humanoid’s MoveDirection multiplied by its WalkSpeed (And it’ll work for all platforms which is neat)

If you need anymore help just lemme know :slightly_smiling_face:

Oh yeah and you’ll need some raycasting to find the normal vector of the ground of where the player is standing at if you’re not already doing that

Thank you, but I already have raycasting and code to align the character with the terrain. What I don’t know how to do is combine the CFrame that aligns them with terrain with the ability to rotate them with their movement. I know how to get the CFrame to align them with terrain, and I know how to get the CFrame to align them with the camera + how to code input to make that happen, but what I don’t know is how to combine these 2 things :sob:

What I have:

What I don’t know how to do (add rotation on the global Y axis)

I don’t need anyone to tell me how to do the entirety of it + I know the amount of coding that goes into coding movement input. What I don’t know is the simple math of How To Rotate A CFrame Horizontally

CFrame:ToWorldSpace is almost there, but again it’s incremental… I know the exact direction I want the player to rotate to, but am unsure how to get them to face it

This is what I’ve achieved using CFrame:ToWorldSpace, but see how the character rotates incrementally (when I press A or D they rotate a little further left or right from their current rotation) when what I want to achieve is to get the CFrame to rotate to an exact direction relative to the camera. I know how to get that direction, but I don’t know how to combine it with the CFrame that aligns them with the terrain

What I think I’m going to try doing is getting the difference in orientation between the 2 CFrames and rotate incrementally by that difference so that they eventually reach the target orientation, but I’m not sure how to do that


I’ll try to share any progress I make I suppose :+1:

I’m not quite sure what you need but if you want to rotate a CFrame along a global/world axis, you need to first convert the global axis into the local/object space of the CFrame (in this case the local space of TerrainAlignment).

local axis = TerrainAlignment:Inverse().YVector -- global Y axis
local angle = CameraLookAt:ToEulerAnglesYXZ() -- only need first value (Y rotation)
local rotatedCFrame = TerrainAlignment * CFrame.fromAxisAngle(axis, angle)

Thank you, but that just made my character spin in endless circles :thinking:

I did however figure this out by getting the difference between the 2 CFrame’s Y orientations and then multiplying them by that difference to get them to line up while still aligning with the terrain in all other ways; I’m not sure if the original post title is still relevant to this solution but it’s exactly what I wanted to figure out! I think I know how to code the rest of the movement system now that I have this basic logic

local TerrainAlignment = CFrame.fromMatrix(Shared.PrimaryPart.CFrame.Position, alignRightVector, alignLookVector:Cross(alignRightVector), -alignLookVector)
local CameraLookAt = CFrame.lookAt(Shared.PrimaryPart.Position, Shared.PrimaryPart.Position + game.Workspace.CurrentCamera.CFrame.LookVector)
-- ^ The CFrame that I want to align the Y orientation and only the Y orientation with
		
local x, y, z = TerrainAlignment:ToOrientation()
local x2, y2, z2 = CameraLookAt:ToOrientation()
local dif = y - y2
		
TerrainAlignment = TerrainAlignment * CFrame.Angles(0, dif, 0)
TerrainTiltGyro.CFrame = TerrainAlignment

Here’s the disorganized as butt prototype coding in case anyone is tackling a similar confusion (obviously a lot to still change about it; I’m just tackling the basics at this stage). GroundSensor.run() is every frame

local GroundSensor  = {}

local Raycasts = require(game.ReplicatedStorage:WaitForChild("Raycasts"))

local Shared = require(script.Parent:WaitForChild("Shared"))

local PrimaryPart: BasePart = Shared.PrimaryPart

local PrimaryPartWidth = PrimaryPart.Size.X
local PrimaryPartThickness = PrimaryPart.Size.Y -- Vertical thickness
local PrimaryPartLength = PrimaryPart.Size.Z

-- The params used to search for a platform
local FindPlatformParams = RaycastParams.new()
FindPlatformParams.CollisionGroup = "Default"
FindPlatformParams.FilterDescendantsInstances = {PrimaryPart.Parent}
FindPlatformParams.FilterType = Enum.RaycastFilterType.Exclude

local SLOPE_DIFFERENTIATION_LIMIT = 2

-- Ordered: RGBV color
local frontRep = Instance.new("Part", game.Workspace)
frontRep.Position = PrimaryPart.Position
frontRep.CanCollide = false
frontRep.Anchored = true
frontRep.CanTouch = false
frontRep.Size = Vector3.new(2, 2, 2)
frontRep.Transparency = .5
frontRep.Color = Color3.new(1, 0, 0)
local frontHit = frontRep:Clone()
frontHit.Parent = game.Workspace
frontHit.Size = Vector3.new(1, 1, 1)
local hindRep = frontRep:Clone()
hindRep.Parent = game.Workspace
hindRep.Position = PrimaryPart.Position
hindRep.Color = Color3.new(0, 0, 1)
local hindHit = hindRep:Clone()
hindHit.Parent = game.Workspace
hindHit.Size = Vector3.new(1, 1, 1)
local hindHit = hindRep:Clone()
hindHit.Parent = game.Workspace
hindHit.Size = Vector3.new(1, 1, 1)
local rightRep = frontRep:Clone()
rightRep.Parent = game.Workspace
local rightHit = rightRep:Clone()
rightHit.Size = Vector3.new(1, 1, 1)
rightHit.Parent = game.Workspace
rightHit.Color = Color3.new(0, 1, 0.0666667)
local leftRep = frontRep:Clone()
leftRep.Parent = game.Workspace
local leftHit = leftRep:Clone()
leftHit.Parent = game.Workspace
leftHit.Size = Vector3.new(1, 1, 1)
leftHit.Color = Color3.new(0.933333, 0, 1)

-- Height to hover off the ground
local hipHeight = .5

local sensorRaycastDistance = hipHeight * 1.5

local Speed = {
	CurrentSpeed = 0,
	TargetSpeed = .5
}

--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Setup

local RootPartAttachment = Instance.new("Attachment")
RootPartAttachment.Name = "RootPartAttachment"
RootPartAttachment.Position = Vector3.new(0, 0, 0)
RootPartAttachment.Parent = PrimaryPart

-- Controls vertical velocity to position character above terrain
local AlignPosition = Instance.new("AlignPosition")
AlignPosition.Parent = PrimaryPart
AlignPosition.Mode = Enum.PositionAlignmentMode.OneAttachment
AlignPosition.Attachment0 = RootPartAttachment
AlignPosition.ForceLimitMode = Enum.ForceLimitMode.PerAxis
AlignPosition.MaxAxesForce = Vector3.new(1000000, 1000000, 1000000)
AlignPosition.Responsiveness = 200

local TerrainTiltGyro = Instance.new("BodyGyro")
TerrainTiltGyro.Parent = PrimaryPart
TerrainTiltGyro.MaxTorque = Vector3.new(1000, 1000, 1000)
TerrainTiltGyro.P = 1000
TerrainTiltGyro.Name = "TerrainTiltGyro"

local BodyVelocity = Instance.new("BodyVelocity")
BodyVelocity.Parent = PrimaryPart
BodyVelocity.MaxForce = Vector3.new(10000,10000,10000)
BodyVelocity.P = 10000

--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Private

-- // The function which reports a successful hit to a raycast searching for a standing platform
local function raycastSuccessPlatformFunction(Result: RaycastResult)
	local Hit = Result.Instance
	if Hit == game.Workspace.Terrain or (Hit:IsA("Part") and Hit.CanCollide) then
		return true
	end
end

local function raycastFront(lookVector: Vector3, raycastVector: Vector3, upVector: Vector3)
	local origin = PrimaryPart.Position + ((PrimaryPartLength/2) * lookVector) + (upVector * 6)
	frontRep.Position = origin
	local Result: RaycastResult? = Raycasts.send(origin, raycastVector, 500, FindPlatformParams, raycastSuccessPlatformFunction)
	if Result then
		frontHit.Position = Result.Position
	end
	return if Result then Result.Position else nil
end

local function raycastBack(lookVector: Vector3, raycastVector: Vector3, upVector: Vector3)
	local origin = PrimaryPart.Position - ((PrimaryPartLength/2) * lookVector) + (upVector * 6)
	hindRep.Position = origin
	local Result: RaycastResult? = Raycasts.send(origin, raycastVector, 500, FindPlatformParams, raycastSuccessPlatformFunction)
	if Result then
		hindHit.Position = Result.Position
	end
	return if Result then Result.Position else nil
end

local function raycastRight(rightVector: Vector3, raycastVector: Vector3, upVector: Vector3)
	--raycastVector = Vector3.new(0, -1, 0)
	local origin = PrimaryPart.Position + ((PrimaryPartWidth/2) * rightVector) + (upVector * 6)
	local Result: RaycastResult? = Raycasts.send(origin, raycastVector, 500, FindPlatformParams, raycastSuccessPlatformFunction)
	if Result then
		rightHit.Position = Result.Position
	end
	return if Result then Result.Position else nil
end

local function raycastLeft(rightVector: Vector3, raycastVector: Vector3, upVector: Vector3)
	--raycastVector = Vector3.new(0, -1, 0)
	local origin = PrimaryPart.Position - ((PrimaryPartWidth/2) * rightVector) + (upVector * 6)
	local Result: RaycastResult? = Raycasts.send(origin, raycastVector, 500, FindPlatformParams, raycastSuccessPlatformFunction)
	if Result then
		leftHit.Position = Result.Position
	end
	return if Result then Result.Position else nil
end

local function getSizeOfTable(t)
	local count = 0
	for k, v in pairs(t) do
		count += 1
	end
	return count
end

local function applyForwardVelocity(Condition: boolean?)
	if Condition == true then
		local speedValue = 15

		local rotation = PrimaryPart.Orientation.Y
		local Xdirection = math.sin(0.0174532925*rotation)*(speedValue * -1)
		local Zdirection = math.cos(0.0174532925*rotation)*(speedValue * -1)
		BodyVelocity.Velocity = Vector3.new(Xdirection, 0, Zdirection) + Vector3.new(0, 0, 0)
	else -- Cease forward movement
		BodyVelocity.Velocity = Vector3.new(0, 0, 0)
	end
end

--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Public

local Forward_Pressed = false
local ContextActionService = game:GetService("ContextActionService")
ContextActionService:BindAction("Forward", function(actionName, inputState)
	if inputState == Enum.UserInputState.Cancel then
		return
	end
	if inputState == Enum.UserInputState.Begin then
		Forward_Pressed = true
		Speed.CurrentSpeed = 0
		--applyForwardVelocity(true)
	else
		Forward_Pressed = false
		--applyForwardVelocity(false)
	end
end, false, Enum.KeyCode.W)

local Left = false
ContextActionService:BindAction("Test", function(actionName, inputState)
	if inputState == Enum.UserInputState.Cancel then
		return
	end
	if inputState == Enum.UserInputState.Begin then
		Left = true
	else
		Left = false
	end
end, false, Enum.KeyCode.A)

local Right = false
ContextActionService:BindAction("Test2", function(actionName, inputState)
	if inputState == Enum.UserInputState.Cancel then
		return
	end
	if inputState == Enum.UserInputState.Begin then
		Right = true
	else
		Right = false
	end
end, false, Enum.KeyCode.D)

function GroundSensor.run()
	-- Reset raycast filter list
	FindPlatformParams.FilterDescendantsInstances = {PrimaryPart.Parent}

	-- // Get Platform Hit Vectors // --
	-- Send vectors downwards to orientate player with the terrain
	local lookVector: Vector3 = Shared.PrimaryPart.CFrame.LookVector
	local rightVector: Vector3 = Shared.PrimaryPart.CFrame.RightVector
	local upVector: Vector3 = Shared.PrimaryPart.CFrame.UpVector
	local downVector: Vector3 = Shared.PrimaryPart.CFrame.UpVector * -1

	local PlatformHits = {
		Front = raycastFront(lookVector, downVector, upVector),
		Back = raycastBack(lookVector, downVector, upVector),
		Right = raycastRight(rightVector, downVector, upVector),
		Left = raycastLeft(rightVector, downVector, upVector)
	}

	local totalPlatformHits = getSizeOfTable(PlatformHits)
	local averagePlatformHit = Vector3.new(0,0,0)
	for i, p in pairs(PlatformHits) do
		averagePlatformHit += p
	end
	averagePlatformHit /= totalPlatformHits


	-- Position character based off the averagePlatformHitd of the sensored positions
	if totalPlatformHits > 0 then

		local height = averagePlatformHit.Y + hipHeight
		local positionVector = PrimaryPart.Position

		-- // Move forward // --
		if Forward_Pressed then
			Speed.CurrentSpeed += 0.003
			if Speed.CurrentSpeed > Speed.TargetSpeed then
				Speed.CurrentSpeed = Speed.TargetSpeed
			end
			positionVector += (lookVector * Speed.CurrentSpeed)
			--applyForwardVelocity(true)
		end

		-- // Determine Alignment // --
		-- Align to platform
		local alignLookVector = (PlatformHits.Front - PlatformHits.Back).unit
		local alignRightVector = (PlatformHits.Left - PlatformHits.Right).unit

		--local Align = CFrame.fromMatrix(Shared.PrimaryPart.CFrame.Position, Shared.PrimaryPart.CFrame.RightVector, Shared.PrimaryPart.CFrame.UpVector, -Shared.PrimaryPart.CFrame.LookVector)  -- THIS IS CORRECT
		local TerrainAlignment = CFrame.fromMatrix(Shared.PrimaryPart.CFrame.Position, alignRightVector, alignLookVector:Cross(alignRightVector), -alignLookVector)
		local CameraLookAt = CFrame.lookAt(Shared.PrimaryPart.Position, Shared.PrimaryPart.Position + game.Workspace.CurrentCamera.CFrame.LookVector)
		
		local x, y, z = TerrainAlignment:ToOrientation()
		local x2, y2, z2 = CameraLookAt:ToOrientation()
		local dif = y - y2
		
		TerrainAlignment = TerrainAlignment * CFrame.Angles(0, dif, 0)
		TerrainTiltGyro.CFrame = TerrainAlignment

		AlignPosition.Position = Vector3.new(positionVector.X, height, positionVector.Z)
	else

	end
end

return GroundSensor