CFraming parts relative to another part's orientation with Raycast normal

Hi there, I’ve been working on a ledge climbing system. I want to detect ledges of a part in order to position the Character’s RootPart to it.

I’ve successfully done this by detecting a part’s normal vector from a Raycast - and then move the RootPart to that ledge. It works, but

The Problem
It only works if the orientation is at 0. It doesn’t respect a part’s orientation. The ledge detection remains the same no matter the orientation and hence the RootPart moves to the opposite / wrong / unwanted offset ledge.

What works

image

What doesn’t
image

As you can see, the rotated part still thinks the orientation is at 0.

I did a lot of digging around in the DevForums regarding part CFrame vectors (LookVectors, UpVectors) to connect other parts - but I found little documentation about how to implement Raycast normals and surfaces, or even orientation. How can I achieve this?

Here’s a snippet for the ledge detection. This is part of a bigger segment in RunService.Heartbeat.

local ray = Ray.new(Head.CFrame.Position, Head.CFrame.LookVector * 3)
local part, position, hitNormal = workspace:FindPartOnRay(ray,Character)

if part then
	local absoluteNormal = Vector3.new(math.abs(hitNormal.X), math.abs(hitNormal.Y), math.abs(hitNormal.Z)) -- the absolute value makes sure I'm not multiplying negatives with negatives (otherwise will detected 2 ledges on a 4 ledged object)
		
	local yVector = (part.CFrame.UpVector) * ((part.Size.Y) / 2)
	local xzVector = hitNormal * ((part.Size * absoluteNormal) / 2)
	local ledgeOffset = part.CFrame + yVector + xzVector
	ledgeOffset = CFrame.lookAt(ledgeOffset.Position, ledgeOffset.Position - hitNormal) -- this makes the detected ledge face the Part

	local ledgePart = Instance.new("Part") -- just a debug to visualize the ledge
	ledgePart.Parent = workspace
	ledgePart.Anchored = true
	ledgePart.Size = Vector3.one
	ledgePart.CFrame = ledgeOffset
	ledgePart.CanQuery = false
	ledgePart.CanCollide = false
	ledgePart.CanTouch = false
end

I think you need to rotate absoluteNormal to match the rotation of the part in order to multiply it with the Size and get the result you are looking for. I think that might be as easy as

absoluteNormal = part.CFrame:VectorToObjectSpace(absoluteNormal)

Don’t quote me on that though. I’ve never seen this approach of yours and it is quite bizarre to me.

The way I would get the position of the ledge for a simple block setup like this is:

  1. Take the raycast’s hit position and translate it to local coordinates of the part. In your code, and also in mine, the racast’s hit position is called position.
local localPos = part.CFrame:PointToObjectSpace(position)
  1. Now in local coordinates of the object, set the y component to the top of the part.
local localLedgePos = Vector3.new(localPos.x, part.Size.y/2, localPos.z)
  1. Convert back to world coordinates.
local ledgePos = part.CFrame:PointToWorldSpace(localLedgePos)
  1. Then construct a CFrame using CFrame.lookAt and the hitNormal like you are already doing.

There are a few limitations of this approach worth mentioning.

  1. Upside down Parts will cause an upside down ledge to be found
  2. Unusual geometry that is not a Part (MeshParts, Unions) will find a ledge somewhere at the top of their bounding box, acting as if they have the same geometry as a plain Part.
  3. Parts stacked on top of one another will find a ledge where they connect even though it is a solid wall to the player

If you are able to work around these limitations / are okay with them, this should be a decent approach to finding ledges.

1 Like

My hero!
You’re a life saver - I think I’m alright with those limitations after all the hoops I tried to jump through

I’d imagine I can offset some of the positioning with UpVectors and LookVectors - this is flawless, thank you

1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.