How to align character to surface normal?

It looks orientation based. Are you trying to allow players to walk up the walls as well? or just have them walk on the ground? If so you can try adding a vector3 value to the CFrame i think?

I’m trying to make the player only align on the floor, not walls or ceilings.

Okay hm, could you try this? I am not home so im unable to boot up studio and verify myself :confused: If i am correct + Vector3.new(0,1,0) should set the rotation to make the character stand upwards. You could also maybe take a browse in roblox’s player movement handler and see how they move their characters if thats accessible im unsure.

local Params = RaycastParams.new()
Params.FilterType = Enum.RaycastFilterType.Exclude
Params.FilterDescendantsInstances = {Character}

RunService.RenderStepped:Connect(function()
    local Raycast = workspace:Raycast(Root.Position, -Root.CFrame.UpVector * 4, Params)
    if Raycast then
        if Raycast.Instance then
            local hitPos = Raycast.Position
            Root.CFrame = CFrame.new(hitPos, hitPos + Vector3.new(0, 1, 0))
        end
    end
end)
1 Like


This is what happens now, very weird behavior.

Ah, well i won’t be much help as i cant use roblox studio to test, Although on the brighter side i did find the core scripts for the Roblox Player check out how the movement is handled, Maybe you can reference this?
– Keyboard Movement

– Master Controller (Handles all player movement positioning)

1 Like

Apologies for the late reply, but i don’t really understand how this can help me with the issue.

Adding the normal to the raycast position at the

Root.CFrame = CFrame.new(Raycast.Position, Raycast.Position + Raycast.Normal)

part is causing the issue

You’re basically telling the character to look at the point where the ray landed plus the normal (which is a vector with a magnitude/length of 1) so it doesn’t change the orientation.

For example let’s say the ray hit at point (100, 100, 100)
and the normal is (0, 1, 0) you’re asking the character to at the raycast hit position at a stud above which is causing the orientation issue

I can’t get on Studio at the moment so I can’t provide any solution to actually fix this issue

1 Like
local RunService = game:GetService("RunService")
local Player = game:GetService("Players").LocalPlayer
local Character = Player.Character or Player.CharacterAdded:Wait()
local Root = Character.PrimaryPart

local Params = RaycastParams.new()
Params.FilterType = Enum.RaycastFilterType.Exclude
Params.FilterDescendantsInstances = {Character}
--------------------------------------
RunService.RenderStepped:Connect(function()
	local Raycast = workspace:Raycast(Root.Position, -Root.CFrame.UpVector * 4, Params)
	if Raycast then
		if Raycast.Instance then
			Root.CFrame = CFrame.lookAt(
				Raycast.Position + Vector3.new(0,3,0),
				Raycast.Position + Vector3.new(0, 3, 0) + Vector3.new(Root.CFrame.LookVector.X, 0, Root.CFrame.LookVector.Z),
				Vector3.new(0, 1, 0)
			)
		end
	end
end)

I was able to get this to work in studio, although the camera is jittery. It aligns the character to the surface. The camera is jittery because it is attached to the player’s character, and its constantly updating the player’s character. If you want i can try and fix this so it doesn’t look as jittery but the player is unable to jump as well.

1 Like

Thanks, and you’re right, the camera is jittery. But the character CAN jump but for some reason isn’t aligning to the surface still.

You can smooth it out using Lerp. Also, consider using a 60Hz loop using
while true do foo() task.wait(1/60) end

Ah i see what you want., let me see if i can mock up a mini area like yours, I was able to jump but not get off the ground only sometimes, but judging by you have a custom jumping i assume that’s why you are able to get off the ground. I never tried something like this so hopefully i can get it.

Yeah but it’s not a custom jump, it’s the regular roblox one.

Where do i put that?

charlimit

Hey, i was just browsing seeing anything else have you ever looked at this? Is this what you want to achieve?

Nah, basically what i wanna do is this:

Like, the humanoidrootpart tilts according to the surface it’s on (as well as the whole body), no momentum gain or physics stuff or whatever. If what the player’s trying to climb up is out of the raycast’s range, then it shouldn’t let him tilt, simple

1 Like

Hi there, you are experiencing this behavior because you are telling the game to set the character’s position (Raycast.Position in your code) to where your raycast hits the floor, and set the character to be facing the position where your raycast hits the floor plus its normal vector.

For example, in world coordinates, if the player was located at (0, 5, 0) and the raycast hit the floor at (0, 0, 0), your code is telling the character to position itself at (0, 0, 0), facing toward (0, 0, 0) + (0, 1, 0) = (0, 1, 0).

Rather than using the lookAt constructor for this, try using Cframe.fromMatrix(). which lets you position/orient your player by passing in its position, right, up, and look vectors. The following code shoots a raycast downward to find the surface normal, repositions the player and treats the normal vector as the character’s UpVector.

-- Local script

-- Should probably run this via Runservice.PreSimulation so the player wont see themselves moving
-- Raycasting is cheap. You should be more than able to do this every frame.

local characterCFrame = playerCharacter.CFrame

local raycastParams = RaycastParams.new()
raycastParams.FilterType = Enum.RaycastFilterType.Exclude
raycastParams.FilterDescendantsInstances = {playerCharacter};

local raycastResult = workspace:Raycast(characterCFrame.Position, characterCFrame.UpVector * -100, --[[Any distance you want]] raycastParams)
if raycastResult then
	
	local upVector = raycastResult.Normal;
	local rightVector = characterCFrame.RightVector;
	local lookVector = characterCFrame.LookVector;
	-- Construct the player's new CFrame
	local finalCFrame = CFrame.fromMatrix(characterCFrame.Position, rightVector, upVector, lookVector);
	
	playerCharacter:PivotTo(finalCFrame);
	
end

This should work nicely for ramps and small gradients. Players tend to fall over at sharper angles, which you could try to fix by using Humanoid:SetStateEnabled(Enum.HumanoidStateType.FallingDown, false) but you may also have to do some trickery with the player’s gravity in order to have them walk up vertical walls.

If all else fails and you don’t want to do it yourself, definitely give EgoMoose’s wall stick implementation a try.

2 Likes

Hi, thanks for the reply and the explanation, i think i understand better now. So, i tried the code you gave me, but i noticed it gave me the error “CFrame is not a valid member of Character”. So, i changed it to Character:GetPivot() instead, and this is what it gives me when i try it in game:


Here’s my code:

RunService.PreSimulation:Connect(function()
    -- Local script

    -- Should probably run this via Runservice.PreSimulation so the player wont see themselves moving
    -- Raycasting is cheap. You should be more than able to do this every frame.

    local characterCFrame = Character:GetPivot()

    local raycastParams = RaycastParams.new()
    raycastParams.FilterType = Enum.RaycastFilterType.Exclude
    raycastParams.FilterDescendantsInstances = {Character};

    local raycastResult = workspace:Raycast(characterCFrame.Position, characterCFrame.UpVector * -100, --[[Any distance you want]] raycastParams)
    if raycastResult then

        local upVector = raycastResult.Normal;
        local rightVector = characterCFrame.RightVector;
        local lookVector = characterCFrame.LookVector;
        -- Construct the player's new CFrame
        local finalCFrame = CFrame.fromMatrix(characterCFrame.Position, rightVector, upVector, lookVector);

        Character:PivotTo(finalCFrame);

    end
end)

Here is how you would do it by modifying C0 in order to avoid upsetting the humanoid auto-orientation, took me an hour due to silly mistakes like forgetting raycast params.

local motor : Motor6D = script.Parent.LowerTorso.Root -- Assume this is a Motor6D
local part0 = motor.Part0 -- Parent part
local part1 = motor.Part1 -- Child part (e.g., head, torso)

local randomAxis = Vector3.new(0,0,1)

local function getRotationBetween(u, v, axis)
	local dot, uxv = u:Dot(v), u:Cross(v)
	if (dot < -0.99999) then return CFrame.fromAxisAngle(axis, math.pi) end
	return CFrame.new(0, 0, 0, uxv.x, uxv.y, uxv.z, 1 + dot)
end


local originalC0 = motor.C0
local originalC1 = motor.C1

local function worldCFrameRotationToC0ObjectSpace(motor6DJoint, worldCFrame)
	local part0 = motor6DJoint.Part0
	local c1Store = motor6DJoint.C1
	local goalC0CFrame = part0.CFrame:Inverse() * worldCFrame * c1Store
	return goalC0CFrame
end
-- your previous code...

-- Create debug part for target rotation
local debugPart = Instance.new("Part")
debugPart.FrontSurface = Enum.SurfaceType.Motor
debugPart.Size = Vector3.new(3, 3, 0.5)
debugPart.Color = Color3.new(1, 0, 0) -- Red
debugPart.Material = Enum.Material.Neon
debugPart.Anchored = true
debugPart.CanCollide = false
debugPart.CanTouch = false
debugPart.CanQuery = false
debugPart.Name = "DebugTarget"
debugPart.Parent = workspace

-- Create another part for the floor normal gizmo
local floorNormalGizmo = Instance.new("Part")
floorNormalGizmo.Size = Vector3.new(0.2, 0.2, 5) -- Thin arrow
floorNormalGizmo.Color = Color3.new(0, 1, 0) -- Green
floorNormalGizmo.Material = Enum.Material.Neon
floorNormalGizmo.Anchored = true
floorNormalGizmo.CanCollide = false
floorNormalGizmo.CanTouch = false
floorNormalGizmo.CanQuery = false
floorNormalGizmo.Name = "FloorNormalGizmo"
floorNormalGizmo.Parent = workspace

--part0.Transparency = 0 -- make sure part0 is visible

--game["Run Service"].PreSimulation:Connect(function(dt)
--	motor.Transform = CFrame.new()
--end)

local params = RaycastParams.new()
params.FilterDescendantsInstances = {script.Parent, floorNormalGizmo, debugPart}

while true do
	local rayResult = workspace:Raycast(part0.Position, -1000 * part0.CFrame.UpVector, params)

	if rayResult then
		local floorNormal = rayResult.Normal
		local hitPosition = rayResult.Position
		
		local angle = part1.CFrame.UpVector:Dot(floorNormal)
		--print(math.deg(math.acos(angle))) IDK
		--if math.deg(math.acos(angle)) < 0.1 then
		--	task.wait()
		--	continue
		--end
		-- Rotate the part1 to align with floor normal
		local x,y,z = part1.CFrame:ToOrientation()
		local part1NewCF = CFrame.fromOrientation(x,y,0)
		local rotateToFloorCFrame = getRotationBetween(part0.CFrame.UpVector, floorNormal, randomAxis)

		-- Target worldspace CFrame
		local targetWorldCFrame = rotateToFloorCFrame * part1.CFrame

		-- Move the debug part to the target
		--debugPart.CFrame = targetWorldCFrame.Rotation + part0.Position + Vector3.new(0,4,0)

		-- Update the Motor6D's C0
		motor.C0 = (part1.CFrame:Inverse() * targetWorldCFrame).Rotation + motor.C0.Position

		-- Update the floor normal gizmo
		--local lookVector = floorNormal
		--local upVector = Vector3.new(0,1,0)
		--if math.abs(lookVector:Dot(upVector)) > 0.99 then
		--	upVector = Vector3.new(1,0,0) -- avoid gimbal lock
		--end
		--local rightVector = upVector:Cross(lookVector).Unit
		--local newUpVector = lookVector:Cross(rightVector).Unit

		--local orientationCFrame = CFrame.lookAlong(hitPosition, floorNormal * 2.5)
		--floorNormalGizmo.CFrame = orientationCFrame
	end
	
	task.wait(0)

end

Thank you for the reply! but the character rig i’m using doesn’t have a lowertorso, it has a single torso which has this joint in it:


So i used this joint instead. I debugged by putting a print inside the while true loop, and it does print, but the character doesn’t rotate

Idk try changing the root priority of the humanoird root part higher.

Otherwise try testing the script in an empty baseplate.

It could also be because your joints and rig are setup differently and the C1 or part1 has some rotation to it which messes up the calculations.

Sounds like something wrong with the raycast at it uses the part0 up vector so the raycast could never hit anything.