Making a part face an object on only one axis in three dimensional space?

So, I’ve been trying to make a spray can tool that let’s you spray decals on any surface. I want it to align with the surface while also rotating on the Y axis in order to look at the player who sprayed it.
The code for the positioning is below and the size of the part is {5, 0.2, 5}. The problem with the code is that it only seems to work half way… Here’s an image to see what I mean: Screenshot by Lightshot It works on the left side and correctly adjusts the Y axis to look at me, but when it get’s too far to the right it does a 360 until it get’s to the left again and is normal. I’ve tried other methods and nothing has worked, the solution is most likely easy but this isn’t my strong suit so yeah… Please help if you can. :slight_smile:

local function calculateSprayInfo()
    -- Get needed spray information and send it off to the server.
    local newRay				= Ray.new(Camera.CFrame.p, (Mouse.Hit.p - Camera.CFrame.p).unit * 200)
    local _, Pos, surfaceNormal	= workspace:FindPartOnRay(newRay)
    local Torso					= Character:FindFirstChild("Torso")
    if not Torso then return end
    local sprayCFrame			= CFrame.new(Pos, Pos - surfaceNormal) * CFrame.Angles(-90 * math.pi/180, 0, 0)
    local lookAt				= CFrame.new(sprayCFrame.p, Torso.Position)
    local cx, cy, cz			= sprayCFrame:toEulerAnglesXYZ()
    local lx, ly, lz			= lookAt:toEulerAnglesXYZ()
    sprayCFrame					= CFrame.new(sprayCFrame.p) * CFrame.Angles(cx, ly, 0)

    sprayEvent:FireServer(assetId, decayTime, spraySize, sprayCFrame)
end

I am confused as to specifically what you want. It sounds like you want the instanced… part/decal to be on the surface of the given part. In that case you do this:

[code]
function getSurfaceNormal(Point, OtherPart, TargetSurface)
local OPCFrame = OtherPart.CFrame
local WorldSpaceNormalIDVector = Vector3.FromNormalId(TargetSurface);
local ObjectSpaceSurfaceDirection = OPCFrame:vectorToWorldSpace(WorldSpaceNormalIDVector);
local Up = Vector3.new(0,1,0)–OPCFrame:vectorToWorldSpace(Vector3.new(0,1,0));

-- Determine if ObjectSpaceSurfaceDirection is equal to Up
if (ObjectSpaceSurfaceDirection == Up) then
	Up = Vector3.new(1,0,0)--OPCFrame:vectorToWorldSpace(Vector3.new(1,0,0));
end

local PerpendicularDirection = ObjectSpaceSurfaceDirection:Cross(Up);
local FrontAxis = ObjectSpaceSurfaceDirection:Cross(PerpendicularDirection);

return CFrame.new(Point.X,Point.Y,Point.Z, 
	PerpendicularDirection.X, ObjectSpaceSurfaceDirection.X, -FrontAxis.X, 
	PerpendicularDirection.Y, ObjectSpaceSurfaceDirection.Y, -FrontAxis.Y, 
	PerpendicularDirection.Z, ObjectSpaceSurfaceDirection.Z, -FrontAxis.Z);

end[/code]

And that will return the CFrame needed to face perpendicular to that surface. The second part about your Y axis rotation confuses me.

Never mind. This should do what you want.

function rotateYAxisToFace(originalPos, lookAtPos) return CFrame.new(originalPos, Vector3.new(lookAtPos.X, originalPos.Y, lookAtPos.z)) end

It works when I spray it on the ground but if I spray it on the wall this happens: Screenshot by Lightshot because it doesn’t maintain it’s state of being perpendicular with the surface which is enforced when sprayCFrame is first defined. I need to be able to use the original sprayCFrame and change only the Y axis so that it correctly rotates to face the player.

Ah yes! I can fix that if you explain to me what should be happening.

Unrelated to your problem: Are you checking, on the server that the parameters supplied through the RemoteEvent by the client are sane/reasonable? If not, exploiters will be able to place sprays with any decayTime, spraySize, and sprayCFrame they want.

Related to your problem: This modified version of your code will give a CFrame with the Z+ direction facing out and the Y- direction facing towards the player. You will need to put your decals on the ‘Front’ face to use it. It might look strange for spraying on walls since images far from the player will be rotated to face them.

Code ``` local function calculateSprayInfo() -- Get needed spray information and send it off to the server. local newRay = Ray.new(Mouse.UnitRay.Origin, Mouse.UnitRay.Direction*200) local _, Pos, surfaceNormal = workspace:FindPartOnRay(newRay) local Torso = Character:FindFirstChild("Torso") if not Torso then return end local sprayCFrame = CFrame.new(Pos, Pos + surfaceNormal) local lookAtInObj = sprayCFrame:pointToObjectSpace(Torso.Position) local faceRad = math.atan2(lookAtInObj.y, lookAtInObj.x) sprayCFrame = sprayCFrame * CFrame.Angles(0, 0, faceRad + math.pi/2)
sprayEvent:FireServer(assetId, decayTime, spraySize, sprayCFrame)

end

</details>
<details><summary>Explanation</summary>
Changes I made:

* I use the UnitRay instead of the positions you were using. It should do the same thing, but this does less math since Mouse already provides us its ray.
* I use `+ surfaceNormal` instead of `- surfaceNormal` so the Z+ is facing outwards from the surface instead of inwards.
* For positioning, I use `pointToObjectSpace` to get the Torso's position relative to the normal CFrame we made. I used `atan2` to get the angle to rotate it by, then I rotated then normal CFrame we made about its Z axis by that rotation + 1/4 of a full rotation, as with testing it appears that was needed.

`atan2` explanation:
`atan2` is like an enhanced version of `atan`, or inverse tan. If you haven't learned about tan/sin/cos and inverse- tan/sin/cos then here is a simple explanation for tan and inverse tan:
A triangle is made up of three sides, which have an angle between each, so three angles. On a right triangle, we can call these `x`, `y`, and `r`. We will call the little symbol between `x` and `r` `theta`.
<img src='//devforum-uploads.s3.dualstack.us-east-2.amazonaws.com/uploads/original/3X/6/8/68b9207236555df0dd50f1cb336223b1de005148.gif'>
`tan(theta)` returns the ratio of `y/x`.
`atan(y/x)` returns `theta`. **a**tan stands for **arc** tan. This is also known as inverse tan, or tan<sup>-1</sup>.
`atan2(y, x)` is the enhanced `atan`.

You can have `-` or `+` of `y` or  `x`, and `atan(y/x)` alone can't differentiate between `-y/-x` and `y/x`. It returns a `theta` between -90 deg and 90 deg. It returns the same thing for `atan(y/x)` and `atan(-y/-x)`, as well as the same for `atan(-y/x)` and `atan(y/-x)`, so it cannot do all 360 deg, only half, or 180 deg.
`atan2(y, x)` will return you the angle around a full circle, considering the `-` and `+` of `y` and `x`, and return you a `theta` between -180 deg and 180 deg.

Because of this, using `atan2` we can get the angle towards a certain point on a 2d plane.
In my modification to your code, I got the Torso's position relative to the normal CFrame we created, then I used its `x` and `y` values, throwing out its `z`. In other words, I put it on the same 2d plane as the normal. Once we have it on the same 2d plane, it's easy to use `atan2` to get the angle we need to face that position.

The only problem with this is it might not actually be what you want. You might want it have the bottom towards the ground and only face the player if it is on the ceiling or floor (aka parallel to the ground, so the bottom can't face the ground any more in one rotation than it can in another).

</details>
<details><summary>Images</summary>
Wall and Floor example
<img src='//devforum-uploads.s3.dualstack.us-east-2.amazonaws.com/uploads/original/3X/3/7/376063c78196aa19125e6a9b440d4a88ba9a0aa0.png'>
Possibly Unwanted Behavior
<img src='//devforum-uploads.s3.dualstack.us-east-2.amazonaws.com/uploads/original/3X/b/a/ba29944ba7d20b72c7b05759de58c0f45833cbf7.jpg'>
Works on any surface. Accidentally clicked my hat a few times
<img src='//devforum-uploads.s3.dualstack.us-east-2.amazonaws.com/uploads/original/3X/6/8/68e69bfcd1b4f01c1812976587ca78ddddb73c85.png'>
Trying it out on an odd shape
<img src='//devforum-uploads.s3.dualstack.us-east-2.amazonaws.com/uploads/original/3X/7/6/76534f0bf70dc45d05a8ca750a410a8f61d0a616.png'>
</details>

Also, unrelated, but Discourse keeps editing my post and adding the text I had above my images after the 'details' spoiler. I tried to undo it and it did it again while I was editing the post.
1 Like

Thanks for the thorough answer/explanation and yes, that’s exactly what I needed.

Using CFrame.new(at, lookAt) is a bad solution to this problem, you can make a very simple solution that always works by using something like a CFrameFromTopBack call (Ctrl + F “CFrameFromTopBack” in here):

local cameraDirectionVector = <...>
local sprayPoint = <...>
local sprayNormal = <...>
local sprayBack = 
    (cameraDirectionVector - sprayNormal*cameraDirectionVector:Dot(sprayNormal).unit
local sprayCFrame = CFrameFromTopBack(sprayPoint, sprayNormal, sprayBack)

Presto… 2 lines of code, no ugly trig, no nonsense. It’s kind of ridiculous that there’s no CFrame from basis vectors call like that in the CFrame API as is.

Why? I frequently use CFrame.new(at, lookAt) in my own code, so if there’s something wrong with it that’d be useful to know.

CFrame.new(point, lookAt) is a very specific case in the much more general problem of constructing a CFrame where one vector is resolutely determined and another vector is a suggestion. Often times, this specific case is not what you want, so you end up fudging it by multiplying by CFrame.Angles() after the fact. If you don’t multiply by CFrame.Angles() afterward, then your approach is pretty good.

As far as solving the general problem, I might declare 6 of these functions (or realistically, declare them as needed):

---@param point The location at which the CFrame is positioned.
---@param backVector The direction in which the back surface faces. Should be normalized.
---@param topVector The direction in which the top surface attempts to face. Should be normalized.
function PointAtZY(point, backVector, topVector)
assert(math.abs(topVector:Dot(backVector)) ~= 1, "backVector and topVector cannot point in the same/opposite direction");
local rightVector = topVector:Cross(backVector).unit;
topVector = backVector:Cross(rightVector);
return CFrame.new(point.x, point.y, point.z, rightVector.x, topVector.x, backVector.x, rightVector.y, topVector.y, backVector.y, rightVector.z, topVector.z, backVector.z);
end

As an example of how it’s used, this would be the “Look At” constructor for CFrame:

---@param point The location at which the CFrame is positioned.
---@param lookAt A position in world space which the negative z vector will point toward.
function CFrame.new(point, lookAt)
local backVector = (point - lookAt).unit;
return PointAtZY(point, backVector, Vector3.new(0, 1, 0));
end

Benefits to using the vector normal approach:

  • No guessing with trig. I’m always guessing with CFrame.Angles.
  • Don’t have to worry about weird artifacts associated with the constructor. An example of those artifacts is if lookAt faces mostly vertically, then it might snap to face exactly vertically. This is generally detectable when you don’t manipulate the CFrame before using it, but if you rotate it a lot before using it, it may just seem like a wild bug.

Another example, to solve the problem posed in the beginning of the thread:

local function calculateSprayInfo()
    -- Get needed spray information and send it off to the server.
    local newRay = Ray.new(Camera.CFrame.p, (Mouse.Hit.p - Camera.CFrame.p).unit * 200)
    local _, Pos, surfaceNormal = workspace:FindPartOnRay(newRay)
    local Torso = Character:FindFirstChild("Torso")
    if not Torso then return end
    --Only the following line is changed:
    local sprayCFrame = PointAtZY(Pos, surfaceNormal, (Torso.Position - Pos).unit);

    sprayEvent:FireServer(assetId, decayTime, spraySize, sprayCFrame)
end

Essentially, though, my solution is a slightly different flavor of Stravant’s solution. The math is more or less the same.

So the only issue with CFrame.new(point, lookAt) is having to tweak the rotation at the end?

Careful, always use an epsilon when comparing floating point numbers! That’s compounded by the fact that a:Dot(b) may actually return a value greater than one or less than -1 even for unit vectors thanks to floating point errors.

assert(math.abs(math.abs(topVector:Dot(backVector)) - 1) > 0.0001, ...)
local Camera = workspace.CurrentCamera
local Player = game.Players.LocalPlayer
local Character = Player.Character or Player.CharacterAdded:Wait()
local Torso = Character:FindFirstChild("Torso")
local Mouse = Player:GetMouse()

local function calculateSprayInfo()
	if not Torso then
		return
	end
	-- Get needed spray information and send it off to the server.
	local newRay				= Ray.new(Camera.CFrame.Position, (Mouse.Hit.Position - Camera.CFrame.Position).Unit * 200)
	local _, Pos, surfaceNormal	= workspace:FindPartOnRay(newRay)
	local sprayCFrame			= CFrame.new(Pos, Pos - surfaceNormal) * CFrame.Angles(math.rad(-90), 0, 0)
	local lookAt				= CFrame.new(sprayCFrame.Position, Torso.Position)
	local cx, cy, cz			= sprayCFrame:ToEulerAnglesXYZ()
	local lx, ly, lz			= lookAt:ToEulerAnglesXYZ()
	local newSprayCFrame		= CFrame.new(sprayCFrame.Position) * CFrame.Angles(cx, ly, 0)

	sprayEvent:FireServer(assetId, decayTime, spraySize, newSprayCFrame)
end

If you were attempting to rotate the CFrame value negative 90 Degrees (pi/2 Radians) around the X-axis. The first CFrame.Angles() constructor function call you made was done incorrectly.

1 Like