How to limit tilt of a CFrame generated from matrix

I’m trying to create a movement system that aligns the player to the terrain

I’ve accomplished aligning the player to the terrain by using raycasts to detect positions and then using CFrame.fromMatrix using vectors generated from those positions

What I can’t for the life of me figure out is how to apply limits to the tilt of the CFrame to prevent the player from, for example, tilting to align with a completely vertical wall. I know how to align the character to the terrain, but I can’t figure out how to limit the steepness of terrain that they can align to

Here’s the code I use to create a CFrame to align the player to the terrain under them. ‘PlatformMatrix’ contains the results of the raycasts that were sent to find points underneath the player, and contains the positions found for front/back/left/right/center, which I then use to generate the lookvector and rightvector

function GroundSensor.getAlignment(RootCFrame: CFrame, PlatformMatrix: {[string]: Vector3}): CFrame
	local alignLookVector = nil

	local forePos = PlatformMatrix.Front or PlatformMatrix.Center
	local backPos = PlatformMatrix.Back or PlatformMatrix.Center
	if forePos ~= backPos then
		alignLookVector = (forePos - backPos).unit
		
		-- My attempt at limiting how severely the player tilts front-back which does not work
		if math.abs(alignLookVector.Y) > FORE_LIMIT_UP then
			alignLookVector = Vector3.new(alignLookVector.X, math.clamp(alignLookVector.Y, FORE_LIMIT_LOW, FORE_LIMIT_UP), alignLookVector.Z)
		end
	end

	local alignRightVector = nil
	local leftPos = PlatformMatrix.Left or PlatformMatrix.Center
	local rightPos = PlatformMatrix.Right or PlatformMatrix.Center
	if leftPos ~= rightPos then
		alignRightVector = (leftPos - rightPos).unit
		
		-- My attempt at limiting how severely the player tilts left-right which does not work
		if math.abs(alignRightVector.Y) > SIDE_LIMIT_UP then
			alignRightVector = Vector3.new(alignRightVector.X, math.clamp(alignRightVector.Y, SIDE_LIMIT_LOW, SIDE_LIMIT_UP), alignRightVector.Z)
		end
	end

	if not alignLookVector or not alignRightVector then
		alignLookVector = -RootCFrame.LookVector
		alignRightVector = RootCFrame.RightVector
	end

	local TerrainAlignment = CFrame.fromMatrix(RootCFrame.Position, alignRightVector, alignLookVector:Cross(alignRightVector), -alignLookVector)
	return TerrainAlignment, alignLookVector.Y
end

However on certain terrain it’s possible for the player to wiggle in such a way that this can happen. I don’t want the player to be able to tilt to the terrain at these extremes, but I can’t figure out how to do that


In the image, the colored squares at the characters’ feet represent where the raycasts hit which is used to generate the raycasts used to orientate the character.

Thank you very much for any help

have you tried to project vectors and validates the final surface normal before applying alignment?

function GroundSensor.getAlignment(RootCFrame: CFrame, PlatformMatrix: {[string]: Vector3}): CFrame
    local MAX_SLOPE_ANGLE = math.rad(45) -- Adjust this value to change maximum slope
    local alignLookVector = nil
    local alignRightVector = nil

    local forePos = PlatformMatrix.Front or PlatformMatrix.Center
    local backPos = PlatformMatrix.Back or PlatformMatrix.Center
    if forePos ~= backPos then
        alignLookVector = (forePos - backPos).unit
        
        local forwardAngle = math.acos(alignLookVector:Dot(Vector3.new(0, 1, 0)))
        if forwardAngle > MAX_SLOPE_ANGLE then
            local rightAxis = alignLookVector:Cross(Vector3.new(0, 1, 0)).Unit
            alignLookVector = (CFrame.fromAxisAngle(rightAxis, MAX_SLOPE_ANGLE) * Vector3.new(0, 1, 0)).Unit
        end
    end

    local leftPos = PlatformMatrix.Left or PlatformMatrix.Center
    local rightPos = PlatformMatrix.Right or PlatformMatrix.Center
    if leftPos ~= rightPos then
        alignRightVector = (leftPos - rightPos).unit
        
        -- Limit left/right tilt using angle-based approach
        local sideAngle = math.acos(alignRightVector:Dot(Vector3.new(0, 1, 0)))
        if sideAngle > MAX_SLOPE_ANGLE then
            local forwardAxis = alignRightVector:Cross(Vector3.new(0, 1, 0)).Unit
            alignRightVector = (CFrame.fromAxisAngle(forwardAxis, MAX_SLOPE_ANGLE) * Vector3.new(0, 1, 0)).Unit
        end
    end

    if not alignLookVector or not alignRightVector then
        alignLookVector = -RootCFrame.LookVector
        alignRightVector = RootCFrame.RightVector
    end

    local TerrainAlignment = CFrame.fromMatrix(
        RootCFrame.Position, 
        alignRightVector, 
        alignLookVector:Cross(alignRightVector), 
        -alignLookVector
    )
    
    return TerrainAlignment, alignLookVector.Y
end
1 Like

That makes the character tilt weirdly


But thank you

i see i think i got the issue i improved the clamping
you can adjust these and let me know if there’s any issue

function GroundSensor.getAlignment(RootCFrame: CFrame, PlatformMatrix: {[string]: Vector3}): CFrame
    --  slope limitations adjust them
    local FORE_LIMIT_UP = 0.5    -- Reduced from previous value adjust it 
    local FORE_LIMIT_LOW = -0.5  -- Reduced from previous value adjust it
    local SIDE_LIMIT_UP = 0.3    -- Reduced for less aggressive side tilt adjust it
    local SIDE_LIMIT_LOW = -0.3  -- Reduced for less aggressive side tilt adjust it

    local alignLookVector = nil

    local forePos = PlatformMatrix.Front or PlatformMatrix.Center
    local backPos = PlatformMatrix.Back or PlatformMatrix.Center
    if forePos ~= backPos then
        alignLookVector = (forePos - backPos).unit
        
        if math.abs(alignLookVector.Y) > FORE_LIMIT_UP then
            local clampedY = math.clamp(alignLookVector.Y, FORE_LIMIT_LOW, FORE_LIMIT_UP)
            local horizontalLength = math.sqrt(alignLookVector.X^2 + alignLookVector.Z^2)
            alignLookVector = Vector3.new(
                alignLookVector.X * (1 - math.abs(clampedY)),
                clampedY,
                alignLookVector.Z * (1 - math.abs(clampedY))
            ).Unit
        end
    end

    local alignRightVector = nil
    local leftPos = PlatformMatrix.Left or PlatformMatrix.Center
    local rightPos = PlatformMatrix.Right or PlatformMatrix.Center
    if leftPos ~= rightPos then
        alignRightVector = (leftPos - rightPos).unit
        
        if math.abs(alignRightVector.Y) > SIDE_LIMIT_UP then
            local clampedY = math.clamp(alignRightVector.Y, SIDE_LIMIT_LOW, SIDE_LIMIT_UP)
            local horizontalLength = math.sqrt(alignRightVector.X^2 + alignRightVector.Z^2)
            alignRightVector = Vector3.new(
                alignRightVector.X * (1 - math.abs(clampedY)),
                clampedY,
                alignRightVector.Z * (1 - math.abs(clampedY))
            ).Unit
        end
    end

    if not alignLookVector or not alignRightVector then
        alignLookVector = -RootCFrame.LookVector
        alignRightVector = RootCFrame.RightVector
    end

    alignRightVector = alignRightVector:Cross(Vector3.new(0, 1, 0)).Unit
    local upVector = alignLookVector:Cross(alignRightVector).Unit
    alignLookVector = alignRightVector:Cross(upVector)

    local TerrainAlignment = CFrame.fromMatrix(
        RootCFrame.Position,
        alignRightVector,
        alignLookVector:Cross(alignRightVector),
        -alignLookVector
    )
    
    return TerrainAlignment, alignLookVector.Y
end

It’s still causing a lot of funky angles,

 -- Constants for slope limitations
    local FORE_LIMIT_UP = 0.5    -- Reduced from previous value adjust it 
    local FORE_LIMIT_LOW = -0.5  -- Reduced from previous value adjust it
    local SIDE_LIMIT_UP = 0.3    -- Reduced for less aggressive side tilt adjust it
    local SIDE_LIMIT_LOW = -0.3  -- Reduced for less aggressive side tilt adjust it

    local alignLookVector = nil

    local forePos = PlatformMatrix.Front or PlatformMatrix.Center
    local backPos = PlatformMatrix.Back or PlatformMatrix.Center
    if forePos ~= backPos then
        alignLookVector = (forePos - backPos).unit
        
        if math.abs(alignLookVector.Y) > FORE_LIMIT_UP then
            local clampedY = math.clamp(alignLookVector.Y, FORE_LIMIT_LOW, FORE_LIMIT_UP)
            local horizontalLength = math.sqrt(alignLookVector.X^2 + alignLookVector.Z^2)
            alignLookVector = Vector3.new(
                alignLookVector.X * (1 - math.abs(clampedY)),
                clampedY,
                alignLookVector.Z * (1 - math.abs(clampedY))
            ).Unit
        end
    end

    local alignRightVector = nil
    local leftPos = PlatformMatrix.Left or PlatformMatrix.Center
    local rightPos = PlatformMatrix.Right or PlatformMatrix.Center
    if leftPos ~= rightPos then
        alignRightVector = (leftPos - rightPos).unit
        
        if math.abs(alignRightVector.Y) > SIDE_LIMIT_UP then
            local clampedY = math.clamp(alignRightVector.Y, SIDE_LIMIT_LOW, SIDE_LIMIT_UP)
            local horizontalLength = math.sqrt(alignRightVector.X^2 + alignRightVector.Z^2)
            alignRightVector = Vector3.new(
                alignRightVector.X * (1 - math.abs(clampedY)),
                clampedY,
                alignRightVector.Z * (1 - math.abs(clampedY))
            ).Unit
        end
    end

    if not alignLookVector or not alignRightVector then
        alignLookVector = -RootCFrame.LookVector
        alignRightVector = RootCFrame.RightVector
    end

    alignRightVector = alignRightVector:Cross(Vector3.new(0, 1, 0)).Unit
    local upVector = alignLookVector:Cross(alignRightVector).Unit
    alignLookVector = alignRightVector:Cross(upVector)

    local TerrainAlignment = CFrame.fromMatrix(
        RootCFrame.Position,
        alignRightVector,
        alignLookVector:Cross(alignRightVector),
        -alignLookVector
    )
    
    return TerrainAlignment, alignLookVector.Y

But tysm for trying!!

np i made it a better vector blending 95 % horizontal and 5 % vertical if you still see any unwanted behavior you can try to adjust the values i added a debugging you can provide me those if it’s still funky

function GroundSensor.getAlignment(RootCFrame: CFrame, PlatformMatrix: {[string]: Vector3}): CFrame
    local FORE_LIMIT_UP = 0.25    
    local FORE_LIMIT_LOW = -0.25  
    local SIDE_LIMIT_UP = 0.15    
    local SIDE_LIMIT_LOW = -0.15  

    local alignLookVector = nil

    local forePos = PlatformMatrix.Front or PlatformMatrix.Center
    local backPos = PlatformMatrix.Back or PlatformMatrix.Center
    if forePos ~= backPos then
        alignLookVector = (forePos - backPos).unit
        warn("[ALIGNMENT] Forward Initial | Y:", alignLookVector.Y, "| Full Vector:", alignLookVector, "| Positions - Front:", forePos, "Back:", backPos)
        
        if math.abs(alignLookVector.Y) > FORE_LIMIT_UP then
            local clampedY = math.clamp(alignLookVector.Y, FORE_LIMIT_LOW, FORE_LIMIT_UP)
            local horizontalPart = Vector3.new(alignLookVector.X, 0, alignLookVector.Z).Unit
            alignLookVector = (horizontalPart * 0.95 + Vector3.new(0, clampedY, 0) * 0.05).Unit
            warn("[ALIGNMENT] Forward Adjusted | Original Y:", alignLookVector.Y, "| Clamped Y:", clampedY, "| Final Vector:", alignLookVector)
        end
    end

    local alignRightVector = nil
    local leftPos = PlatformMatrix.Left or PlatformMatrix.Center
    local rightPos = PlatformMatrix.Right or PlatformMatrix.Center
    if leftPos ~= rightPos then
        alignRightVector = (leftPos - rightPos).unit
        warn("[ALIGNMENT] Side Initial | Y:", alignRightVector.Y, "| Full Vector:", alignRightVector, "| Positions - Left:", leftPos, "Right:", rightPos)
        
        if math.abs(alignRightVector.Y) > SIDE_LIMIT_UP then
            local clampedY = math.clamp(alignRightVector.Y, SIDE_LIMIT_LOW, SIDE_LIMIT_UP)
            local horizontalPart = Vector3.new(alignRightVector.X, 0, alignRightVector.Z).Unit
            alignRightVector = (horizontalPart * 0.95 + Vector3.new(0, clampedY, 0) * 0.05).Unit
            warn("[ALIGNMENT] Side Adjusted | Original Y:", alignRightVector.Y, "| Clamped Y:", clampedY, "| Final Vector:", alignRightVector)
        end
    end

    if not alignLookVector or not alignRightVector then
        alignLookVector = -RootCFrame.LookVector
        alignRightVector = RootCFrame.RightVector
        warn("[ALIGNMENT] Using Default Vectors | Look:", alignLookVector, "| Right:", alignRightVector)
    end

    warn("[ALIGNMENT] Vector Relations | Look·Right:", alignLookVector:Dot(alignRightVector), 
         "| Look·Up:", alignLookVector:Dot(Vector3.new(0,1,0)), 
         "| Right·Up:", alignRightVector:Dot(Vector3.new(0,1,0)))

    local upVector = Vector3.new(0, 1, 0)
    alignRightVector = alignLookVector:Cross(upVector).Unit
    alignLookVector = alignRightVector:Cross(upVector).Unit

    local TerrainAlignment = CFrame.fromMatrix(
        RootCFrame.Position,
        alignRightVector,
        upVector,
        -alignLookVector
    )
    
    local x, y, z = TerrainAlignment:ToEulerAnglesXYZ()
    warn("[ALIGNMENT] Final | Angles(deg):", 
         math.deg(x), math.deg(y), math.deg(z), 
         "| Position:", TerrainAlignment.Position)
    
    return TerrainAlignment, alignLookVector.Y
end

This is causing the character to no longer align with any slope, even when I change the values :pensive:


But thank you so much for your help!

I found out that I can flatten the character’s CFrame when going up too severe of a slope, which accomplishes my ultimate problem of preventing the character from scaling slopes or walking sideways. It’s not a solution to what I created this thread for / being able to limit thir tilt would have more fluid movement than just flattening it entirely, but at least this helps with the end problem


local function getAngle(normal)
	return -(math.deg(math.acos(normal:Dot(Vector3.yAxis)))-90)
end

function GroundSensor.flatten(RootCFrame: CFrame, lookVector: Vector3, rightVector: Vector3)
	lookVector = Vector3.new(lookVector.X, 0, lookVector.Z).Unit
	rightVector = Vector3.new(rightVector.X, 0, rightVector.Z)
	return CFrame.fromMatrix(RootCFrame.Position, rightVector, -lookVector:Cross(rightVector), lookVector)
end

-- // Get Alignment
--   = TerrainAlignment CFrame
function GroundSensor.getAlignment(RootCFrame: CFrame, PlatformMatrix): CFrame
	local alignLookVector = nil
	local facingSlope, sideSlope = nil, nil

	local forePos = PlatformMatrix.Front or PlatformMatrix.Center
	local backPos = PlatformMatrix.Back or PlatformMatrix.Center
	if forePos ~= backPos then
		alignLookVector = (forePos - backPos).Unit
		local angle = getAngle(alignLookVector)
		if angle > 40 or angle < -40 then
			alignLookVector = nil
		else
			sideSlope = angle
		end
	end

	local alignRightVector = nil
	local leftPos = PlatformMatrix.Left or PlatformMatrix.Center
	local rightPos = PlatformMatrix.Right or PlatformMatrix.Center
	if leftPos ~= rightPos then
		alignRightVector = (leftPos - rightPos).unit
		local angle = getAngle(alignRightVector)
		if angle > 40 or angle < -40 then
			alignRightVector = nil
		else
			sideSlope = angle
		end
	end

	if not alignLookVector or not alignRightVector then
		-- Flatten
		local TerrainAlignment = GroundSensor.flatten(RootCFrame, RootCFrame.LookVector, RootCFrame.RightVector)
		return TerrainAlignment, facingSlope, sideSlope
	end

	local TerrainAlignment = CFrame.fromMatrix(RootCFrame.Position, alignRightVector, alignLookVector:Cross(alignRightVector), -alignLookVector)
	return TerrainAlignment, facingSlope, sideSlope
end