Aligning a part relevant to 4 rays

Hello Everyone! I am making a skateboard game and I am struggling to align the skateboard to a a surface. I made a decision that i will use 4 raycasts in each corner of the skateboard (i will call it a part just so its easier to understand. Also i use “alignOrientation” object for rotation). Now here comes the tough part - After getting the normal vector of all 4 rays, idk how to rotate the part acording to them. I’ve tried to get the avg normal vector by doing this (n1 + n2 + n3 + n4) / 4. It works but it for some reason gives a default rotation for a flat surface of (90,90,0). So i suppose this does not work. Here’s an image of my part. Also if anyone reads this, please make it so the calculations EXCLUDE the Y axis, since i don’t want it overlapping with other scripts.
image

2 Likes

Probably a ton of complicated math, try looking into Cross Product, that might help with the math.

My idea is, use the 4 rays as hit points that contact the surface, then make 2 vectors, one for the width and the length, basically,

front right wheel - front left wheel.

and

front right wheel - back right wheel

Then get the normal by getting the Cross Product of these 2 vectors. And that would be your normal, it would be more accurate than averaging.

Sorry my math is not that good. Can you maybe make a tiny code sample showcasing what you just said - mostly the “then make 2 vectors, one for the width and the length” part. Does this method involve normal vectors in the calculations or is the final result the normal vector? Thanks.

Here something like this:

-- Assuming your raycasts gave you these hit positions for each wheel
local frontRightPos = Vector3.new(5, 0, 5)   -- Example position
local frontLeftPos = Vector3.new(-5, 0, 5)   -- Example position
local backRightPos = Vector3.new(5, 0, -5)   -- Example position

--Make the width vector (front right to front left)
local widthVector = frontLeftPos - frontRightPos

--Make the length vector (front right to back right)
local lengthVector = backRightPos - frontRightPos

--Get the surface normal using cross product
local surfaceNormal = widthVector:Cross(lengthVector).Unit

The surfaceNormal variable is the final result/normal vector

1 Like

Thanks a lot. Will def try it!!

1 Like

I tried it and constructed a code that looks like this . Is it correct?

local function alignPartToSurface()
	local ray1 = workspace:Raycast(raycastOrigins["LeftFront"], Vector3.new(0, -5, 0), raycastParams)
	local ray2 = workspace:Raycast(raycastOrigins["RightFront"], Vector3.new(0, -5, 0), raycastParams)
	local ray3 = workspace:Raycast(raycastOrigins["RightBack"], Vector3.new(0, -5, 0), raycastParams)
	
	if ray1 and ray2 and ray3 then
		local widthVector = ray1.Position - ray2.Position

		--Make the length vector (front right to back right)
		local lengthVector = ray3.Position - ray2.Position

		--Get the surface normal using cross product
		local surfaceNormal = widthVector:Cross(lengthVector).Unit
		
		--[[local alignedCFrame = CFrame.fromMatrix(
			part.Position,
			part.CFrame.RightVector,
			surfaceNormal -- Ensure the normal aligns properly
		)]]
		
		bodyGyro.CFrame = CFrame.lookAt(part.Position, part.Position + surfaceNormal)
		
	end
end

Edit : I tried using the commented part - the CFrame.fromMatrix part and it seems to always keep a straight up 90 degree angle on any surface its on

It’s on the right track, the width vector should be right - left, not the other way around. And I would recommend you use your commented code, I think its a little better in my opinion.

Tried it and it is still not working. Here’s a video ( the two visible attachments are the two front ones)

Here’s what the gyro shows on flat ground
image

Try this version:

local function alignPartToSurface()
   local ray1 = workspace:Raycast(raycastOrigins["LeftFront"], Vector3.new(0, -5, 0), raycastParams)
   local ray2 = workspace:Raycast(raycastOrigins["RightFront"], Vector3.new(0, -5, 0), raycastParams)
   local ray3 = workspace:Raycast(raycastOrigins["RightBack"], Vector3.new(0, -5, 0), raycastParams)
   
   if ray1 and ray2 and ray3 then
       local widthVector = ray2.Position - ray1.Position
       local lengthVector = ray3.Position - ray2.Position
       
       local surfaceNormal = widthVector:Cross(lengthVector).Unit
       if surfaceNormal.Y < 0 then
           surfaceNormal = -surfaceNormal
       end
       
       local currentUp = part.CFrame.UpVector
       local rotationAxis = currentUp:Cross(surfaceNormal).Unit
       local rotationAngle = math.acos(math.clamp(currentUp:Dot(surfaceNormal), -1, 1))
       
       local currentYRotation = math.atan2(part.CFrame.LookVector.X, part.CFrame.LookVector.Z)
       bodyGyro.CFrame = CFrame.new(part.Position) * 
                        CFrame.fromAxisAngle(rotationAxis, rotationAngle) * 
                        CFrame.Angles(0, currentYRotation, 0)
   end
end

Same thing. The only real rotation is if ray3 is detecting a not 90 degree surface and the two front rays are detecting a straight surface
image

Screenshot 2024-12-26 162948

Interesting, I guess add more checks? Test this, if this doesn’t work, I will look into the calculations more:

local function alignPartToSurface()
    local ray1 = workspace:Raycast(raycastOrigins["LeftFront"], Vector3.new(0, -5, 0), raycastParams)
    local ray2 = workspace:Raycast(raycastOrigins["RightFront"], Vector3.new(0, -5, 0), raycastParams)
    local ray3 = workspace:Raycast(raycastOrigins["RightBack"], Vector3.new(0, -5, 0), raycastParams)
    
    if ray1 and ray2 and ray3 then
        local widthVector = ray2.Position - ray1.Position
        local lengthVector = ray3.Position - ray2.Position
        
        if widthVector.Magnitude == 0 or lengthVector.Magnitude == 0 then
            return
        end
        
        local surfaceNormal = lengthVector:Cross(widthVector).Unit
        if surfaceNormal.Y < 0 then
            surfaceNormal = -surfaceNormal
        end
        
        local currentUp = part.CFrame.UpVector
        local dot = math.clamp(currentUp:Dot(surfaceNormal), -1, 1)
        local rotationAngle = math.acos(dot)
        
        if math.abs(rotationAngle) < 1e-4 then
            return
        end
        
        local rotationAxis = currentUp:Cross(surfaceNormal).Unit
        if rotationAxis.Magnitude == 0 then
            return
        end
        
        local currentRotation = part.CFrame - part.CFrame.Position
        bodyGyro.CFrame = CFrame.new(part.Position) * 
                          CFrame.fromAxisAngle(rotationAxis, rotationAngle) * 
                          currentRotation
    end
end

Uhh it kinda worked. When i launch the game, the part gets aligned with the other part that its on but then it just stops aligning when i move it over to other parts, The good thing is that this time it aligns perfectly with the first detected part

Screenshot 2024-12-26 164217

image

Try putting it in a Run Service loop, if it isnt already.

Imma try to add print checks and see if that helps (regarding your question :


)

1 Like

Try visualising the origins, printing what the raycasts detected, and the CFrame of the BodyGyro, see if you find any anomalies.

Interesting! When i move the part to another slope, it stops printing the instances, and it only does it if i manually move the part with the roblox studio select tool ( or with any other thing that moves the part)

image

try this:

local RunService = game:GetService("RunService")
local lastPosition = part.Position

local function alignPartToSurface()
    local raycastOrigins = {
        ["LeftFront"] = part.Position + part.CFrame:VectorToWorldSpace(Vector3.new(-1, 0, -1)),
        ["RightFront"] = part.Position + part.CFrame:VectorToWorldSpace(Vector3.new(1, 0, -1)),
        ["RightBack"] = part.Position + part.CFrame:VectorToWorldSpace(Vector3.new(1, 0, 1))
    }

    local ray1 = workspace:Raycast(raycastOrigins["LeftFront"], Vector3.new(0, -5, 0), raycastParams)
    local ray2 = workspace:Raycast(raycastOrigins["RightFront"], Vector3.new(0, -5, 0), raycastParams)
    local ray3 = workspace:Raycast(raycastOrigins["RightBack"], Vector3.new(0, -5, 0), raycastParams)
    
    if ray1 and ray2 and ray3 then
        local widthVector = ray2.Position - ray1.Position
        local lengthVector = ray3.Position - ray2.Position
        
        if widthVector.Magnitude == 0 or lengthVector.Magnitude == 0 then
            return
        end
        
        local surfaceNormal = lengthVector:Cross(widthVector).Unit
        if surfaceNormal.Y < 0 then
            surfaceNormal = -surfaceNormal
        end
        
        local currentUp = part.CFrame.UpVector
        local dot = math.clamp(currentUp:Dot(surfaceNormal), -1, 1)
        local rotationAngle = math.acos(dot)
        
        if math.abs(rotationAngle) < 1e-4 then
            return
        end
        
        local rotationAxis = currentUp:Cross(surfaceNormal).Unit
        if rotationAxis.Magnitude == 0 then
            return
        end
        
        local currentRotation = part.CFrame - part.CFrame.Position
        bodyGyro.CFrame = CFrame.new(part.Position) * 
                          CFrame.fromAxisAngle(rotationAxis, rotationAngle) * 
                          currentRotation
    end
end

RunService.Heartbeat:Connect(function()
    if (part.Position - lastPosition).Magnitude > 0.01 then
        alignPartToSurface()
        lastPosition = part.Position
    end
end)