Hey man, I’m volunteering my time like everyone else on this forum.
Anyways, yes.
This version of the function returns a cframe where the RightVector
and -LookVector
are parallel to the surface, and the UpVector
is pointing out from the surface. The second returned value is the size of the surface in a Vector2 this time for clarity.
To be clear: the CFrame this returns has a positive X-axis which points “to the right”, a positive Z-axis which points “down”, and a positive Y-axis which points “out of the page”.
However, the size returned is a Vector2, which means that it’s X-axis is aligned with the CFrame’s X-axis, but the size-X-axis is aligned with the cframe-Z-axis.
Visualization of the CFrame:
-- Returns a CFrame and a Size (Vector2) of the given surface.
--
-- The CFrame:
-- * is positioned at the center of the surface
-- * has an UpVector = the surface normal
-- * has a LookVector aligned with a part edge, and pointing generally upwards in world space.
local function GetWorldOrientedSurface(part: BasePart, normalId: Enum.NormalId): (CFrame, Vector2)
local cf = part.CFrame
local rot = cf - cf.Position
local nObject = Vector3.fromNormalId(normalId)
local nWorld = rot * nObject
-- get orthogonal vector by utilizing the order of NormalId enums
-- i.e. Front.Value is 5 -> (5+1)%6 = 0 -> Right.Value
local xWorld = rot * Vector3.fromNormalId((normalId.Value + 1) % 6)
-- get other orthogonal vector
local zWorld = nWorld:Cross(xWorld)
-- make them both point "generally down"
if xWorld.Y > 0 then xWorld = -xWorld end
if zWorld.Y > 0 then zWorld = -zWorld end
-- choose the one pointing "more down" one as the z axis for the surface
if xWorld.Y < zWorld.Y then zWorld = xWorld end
-- redefine x axis based on that
xWorld = nWorld:Cross(zWorld)
local surfaceRot = CFrame.fromMatrix(Vector3.new(), xWorld, nWorld, zWorld)
-- get width of part in direction of x and y
local sizeInWorldSpace = rot * part.Size
local sizeInSurfaceSpace = surfaceRot:Inverse() * sizeInWorldSpace
-- get position on surface
local surfaceCFrame = surfaceRot + cf.Position + nWorld * math.abs(sizeInSurfaceSpace.Y) / 2
return surfaceCFrame, Vector2.new(math.abs(sizeInSurfaceSpace.X), math.abs(sizeInSurfaceSpace.Z))
end
-- USAGE
local player = game.Players.LocalPlayer
local mouse = player:GetMouse()
local p = Instance.new("Part")
p.CanCollide = false
p.Anchored = true
p.Transparency = 0.5
p.BrickColor = BrickColor.Red()
local decal = Instance.new("Decal")
decal.Texture = "http://www.roblox.com/asset/?id=67616959" -- some random "this side up" image
decal.Face = Enum.NormalId.Top -- always the top face
decal.Parent = p
p.Parent = player.Character or player.CharacterAdded:Wait()
game:GetService("RunService").Stepped:Connect(function()
local part = mouse.Target
if part then
local cframe, size = GetWorldOrientedSurface(part, mouse.TargetSurface)
p.Size = Vector3.new(size.X, 0.1, size.Y)
p.CFrame = cframe
end
end)