In order to use trigonometry to solve a problem in 3 dimensions, you’ll need to somehow turn the problem into a 2-dimensional problem.
If the original problem is in XY-plane, XZ-plane (@ExercitusMortem’s example) or YZ-plane, then turning it into a 2-dimensional problem can be done in the way @ExercitusMortem mentioned, by ignoring the irrelevant dimension.
However, not all situations are like this. Sometimes turning the problem into a 2-dimensional one is more complicated.
Let’s say you have a turret that can rotate around the y-axis of its base and its own x-axis (which changes based on rotation around the base). You may want to limit its rotation around it’s x-axis to be between some angles relative to it’s default rotation relative to the base.
You probably have a target position you want the turret to point towards. First we can simplify the problem by calculating where this position is relative to the CFrame whose position is the rotation point (assuming the rotation axes intersect) and rotation is the default orientation of the turret. If you are using a motor6D for rotating the turret, the aforementioned CFrame (which should be in world space) should be equal to Motor6D.Part0.CFrame * Motor6D.C0
. This CFrame is called rotationPointCFrameInWorldSpace
in the code below.
local rotationPointCFrameInWorldSpace = -- the aforementioned CFrame
local targetPositionInWorldSpace = -- the target position
local relativeTargetPosition = rotationPointCFrameInWorldSpace:ToObjectSpace(targetPositionInWorldSpace)
If we had no rotation limits, we could just simply use CFrame.lookAt. But when we do have limits, it’s not that easy.
No trigonometry has been used yet, but next it’ll be time for that. You can calculate the angle between the vector from rotationPointCFrameInWorldSpace.Position to targetPositionInWorldSpace and the XZ-plane of rotationPointCFrameInWorldSpace
(this plane is defined by the right vector and look vector of rotationPointCFrameInWorldSpace
) by using the XZ-plane length of the vector describing the relative target position (relativeTargetPosition
) and the y-component of relativeTargetPosition
.
local distanceInLocalXZPlane = Vector2.new(relativeTargetPositon.X, relativeTargetPosition.Z).Magnitude
local angleBetweenTargetOffsetVectorAndLocalXZPlane = math.atan2(relativeTargetPosition.Y, distanceInLocalXZPlane)
We just turned a three dimensional problem into a two-dimensional one by kind of treating the XZ-direction of relativeTargetPositon as one axis and the y-direction in the same coordinate system (which is defined by rotationPointCFrameInWorldSpace
) as the other axis of a two-dimensional coordinate system.
Next, we can calculate an angle that respects the limits.
local validRotationAngleAroundTurretXAxis = math.clamp(angleBetweenTargetOffsetVectorAndLocalXZPlane, minAngle, maxAngle)
Now, we can calculate the relative rotation CFrame (relative to rotationPointCFrameInWorldSpace
) using the CFrame.Angles
constructor.
We need a rotation CFrame that first rotates around the y-axis of rotationPointCFrameInWorldSpace
and then rotates around the x-axis of the CFrame resulting from applying this rotation.
If we have a matrix A, we get a matrix rotated around its own axes by a rotation defined by matrix B by calculating A * B. Calculating a CFrame rotated around the axes of A * B by a rotation defined by matrix C is done by calculating (A * B) * C. Although matrix operations have stricter equality rules than operations on numbers, (A * B) * C is still equal to A * (B * C). Thus, we can combine both of the aforementioned rotations into one rotation CFrame and set this CFrame as the value of Motor6D.Transform. By aforementioned rotations I mean rotation around the y-axis of `rotationPointCFrameInWorldSpace and rotation around the x-axis of the CFrame resulting from applying this rotation.
FCFrame for the rotation around the y-axis of rotationPointCFrameInWorldSpace is CFrame.Angles(0, math.atan2(relativeTargetPosition.X, relativeTargetPosition.Z), 0)
. This is the rotation B in the equation.
The x-axis rotation CFrame (C) is CFrame.Angles(validRotationAngleAroundTurretXAxis, 0, 0)
.
turretRotationMotor6D.Transform = CFrame.Angles(0, math.atan2(relativeTargetPosition.X, relativeTargetPosition.Z), 0) * CFrame.Angles(validRotationAngleAroundTurretXAxis, 0, 0)
Now, we didn’t use much trigonometry (only atan2 a couple of times). However, we don’t need to use CFrame.Angles. Alternatively, with some more trigonometry, we can calculate the axes of Motor6D.Transform that results in the correct rotation and use CFrame.fromMatrix. The axes should be unit vectors (length 1).
Let’s start with the look vector. It should have the same XZ-direction as relativeTargetPosition. The angle between its look vector and the XZ plane should be validRotationAngleAroundTurretXAxis
. Cosine and sine are the x and y position of a point on the unit circle on xy-plane (the center of this unit circle is origo and the radius is 1). Thus, Vector2.new(cos(angle), sin(angle)) is a unit vector. We need a three-dimensional vector, though, so we need a vector whose horizontal component (projection to XZ-plane) has the absolute value of cos(angle) as its length and the horizontal direction of relativeTargetPosition
as its direction. The vertical component (projection to Y-axis) should be sin(angle).
We get a XZ-plane unit vector with the same XZ-direction as relativeTargetPosition
by dividing Vector3.new(relativeTargetPosition.X, 0, relativeTargetPosition’Z) by its length which we already calculated before (distanceInLocalXZPlane
). By multiplying this vector by cos(angle), we get the desired XZ-component of the look vector.
local lookVector = Vector3.new(relativeTargetPosition.X, 0, relativeTargetPosition.Z) / distanceInLocalXZPlane * cos(validRotationAngleAroundTurretXAxis) + Vector3.new(0, sin(validRotationAngleAroundTurretXAxis), 0)
Next it’s time to calculate the up vector. The angle between the upvector and the y-axis should be the same as the angle between the look vector and the XZ-plane, and the up vector should be perpendicular to the look vector.
local upVector = Vector3.new(relativeTargetPosition.X, 0, relativeTargetPosition.Z) / distanceInLocalXZPlane * (-sin(validRotationAngleAroundTurretXAxis)) + Vector3.new(0, cos(validRotationAngleAroundTurretXAxis), 0)
A picture may help in understanding the formula for the up vector. Thinking about two-dimensional vectors instead of 3-dimensional ones helped in thinking of how to edit the look vector formula to get the up vector formula.
Here's a way to think of how the look vector and up vector are calculated
We first form the unit vector of the horizontal component of the look vector (let’s call this h (horizontal)) and the unit vector of the positive y-axis (let’s call this v (vertical)). Let’s also define a = Vector2.new(cos(angle), sin(angle)) and b = Vector2.new(-sin(angle), cos(angle)). These two vectors are from the picture.
The look vector is h * a.X + v * a.Y. The up vector is h * b.X + v * b.Y.
The right vector should be perpendicular to both the look vector and the up vector. It should also point to the right, not to the left (both right and left are perpendicular to front and up directions). We can calculate it as the unit of the cross product of look vector and up vector. The order of these in the cross product is important because with the incorrect order, the resulting vector would point to the left.
local rightVector = lookVector:Cross(upVector).Unit
Now, it’s time to construct the CFrame. As it’s a CFrame defining only a (relative) rotation, its position should be the zero vector.
turretRotationMotor6D.Transform = CFrame.fromMatrix(Vector3.zero, rightVector, upVector, lookVector)