Hmm. Oops. That didn’t work like what I thought it would. That’ll teach me to test stuff before making claims. Sorry about that.
Here’s a test that’ll work on tilted planes. I was running it from a Script in ReplicatedStorage with RunContext set to client. It’s just a client-side test so no Remote Events to make the parts vis on the server.
Hacky test code used for video
This uses the mouse to world ray for the facing vector rather than the player’s hrp. It’s a bit hacky, much pasted from example code from the Roblox docs for raycast and mouse input. Can throw a few rotated Parts around a place and just click to place (it makes a little horseshoe barrier). The cf it creates might not be intuitive if you try placing on the underside of things, but it may give you some ideas for things to try.
--[[
Client-side only example
]]
local UserInputService = game:GetService("UserInputService")
local Players = game:GetService("Players")
local player = Players.LocalPlayer
local character = player.Character
if not character or character.Parent == nil then
character = player.CharacterAdded:Wait()
end
local hrp = character:WaitForChild("HumanoidRootPart")
local directionVector
local MAX_MOUSE_DISTANCE = 1000
local FIRE_RATE = 0.3
local timeOfPreviousPlacement = 0
local rand = Random.new()
local function makeBarrier()
local offset = CFrame.new(0, 0, 6)
local model = Instance.new("Model")
local part = Instance.new("Part")
part.Size = Vector3.new(5, 1, 2)
part.Anchored = true
local fn = function()
local step = 5
for i = 0, step-1 do
local c = part:Clone()
local rot = i * math.pi/(step-1)
local cf = CFrame.Angles(0, rot, 0)
c.CFrame = cf:ToWorldSpace(offset)
c.Size = c.Size * Vector3.new(1,1+3*rand:NextNumber(),1)
c.Position = Vector3.new(c.Position.X, c.Size.Y/2, c.Position.Z)
c.Parent = model
end
local extSize = model:GetExtentsSize()
model.WorldPivot -= Vector3.new(0, extSize.Y/2, 0)
model.WorldPivot *= CFrame.Angles(0, math.pi/4, 0)
model.Parent = workspace
end
fn()
return model
end
-- Check if enough time has passed
local function canPlaceObject()
local currentTime = tick()
if currentTime - timeOfPreviousPlacement < FIRE_RATE then
return false
end
return true
end
local function getWorldRaycast()
local mouseLocation = UserInputService:GetMouseLocation()
-- Create a ray from the 2D mouse location
local screenToWorldRay = workspace.CurrentCamera:ViewportPointToRay(mouseLocation.X, mouseLocation.Y)
-- The unit direction vector of the ray multiplied by a maximum distance
directionVector = screenToWorldRay.Direction * MAX_MOUSE_DISTANCE
-- Raycast from the ray's origin towards its direction
local raycastResult = workspace:Raycast(screenToWorldRay.Origin, directionVector)
if raycastResult then
return raycastResult
else
-- No object was hit
return nil
end
end
local function getSurfaceAlignedCf(surfaceNormal, itemPosition, desiredForward)
-- Calculate the right vector (perpendicular to both forward and up vectors)
local rightVector = surfaceNormal:Cross(desiredForward).Unit
-- Recalculate the forward vector to ensure orthogonality
local forwardVector = rightVector:Cross(surfaceNormal).Unit
-- Create the new CFrame with the position and orientation
local newCFrame = CFrame.fromMatrix(
itemPosition,
rightVector,
surfaceNormal,
forwardVector
)
return newCFrame
end
local function placeItem()
if not canPlaceObject() then
return
end
local rcResult = getWorldRaycast()
if rcResult then
local model = makeBarrier()
local rcSurfNorm = rcResult.Normal
local rcPosition = rcResult.Position
--local upVector = Vector3.new(0,1,0)
local lookVector = Vector3.new(directionVector.X, 0, directionVector.Z).Unit
local cf = getSurfaceAlignedCf(rcSurfNorm, rcPosition, lookVector)
--local cf = CFrame.lookAlong(rcPosition, lookVector, sfNorm)
model:PivotTo(cf)
end
end
UserInputService.InputBegan:Connect(function(input, _gameProcessedEvent)
if input.UserInputType == Enum.UserInputType.MouseButton1 then
placeItem()
end
end)
Cleaned up ver of the key function:
local function lookAlongSurfaceCf(position: Vector3, forwardDir: Vector3, upDir: Vector3): CFrame
-- Normalize given vector for up direction
local upVector = upDir.Unit
-- Calculate the right vector (perpendicular to both forward and up vectors)
-- may need -forwardDir if results are backwards for your use case
--local rightVector = upVector:Cross(-forwardDir).Unit
local rightVector = upVector:Cross(forwardDir).Unit
-- Recalculate the forward vector to ensure orthogonality
local lookVector = rightVector:Cross(upVector).Unit
-- Create CFrame from the position and orientation vectors
local cf = CFrame.fromMatrix(position, rightVector, upVector, lookVector)
return cf
end
This calculates a rightVector for a new cf using cross product of the given forward and up vectors. [your original forward and up vectors need not be at 90 deg angles to one another. The cross product will still find the correct rightVector that is orthogonal to the others.] The code then calculates a new lookVector from the right and up vectors, effectively tilting the forward vector to match with your chosen up vector. The resulting cf will “look along” a surface in the fw direction if you provide a surface normal as the up direction.