< Back 

Blocks Simulator: Rotate

2022-12-26

The rotate tool turns a block around its own axes with arc handles and snaps the angle to the nearest increment. This article covers the order of multiplication that picks the axes, the rounding, why three numbers are enough for the server, and why the three angle fields are three different kinds of rotation.

LuauRobloxGameplayMath
Go to quick view

Turning a block on its own axes.

Abstract. The rotate tool (C) puts three rings on the selected block, one per axis of the block itself. Dragging a ring turns the block around that axis, and the angle snaps to the nearest multiple of an increment, 15° by default. The code is short. It saves the block’s CFrame when the button goes down and multiplies it on the right by a rotation of the snapped angle, and that order is what makes the block turn around its own axes and around its centre. In the script I left a comment that says “this rotation system was too complex for me to do, #stackoverflow4ever”, and the snapping function is the same in the version I wrote as juank, so I didn’t derive this part myself. This article explains why it works, what the server receives (three numbers) and why that’s enough, and what the three angle fields in the window really do, which turned out to be three different things.

1. The problem

The tool uses ArcHandles. When a ring is dragged, they report its axis and the angle turned since the button went down, in radians. The block has to turn by that angle, snapped, around its own axis, and stay where it is, because a block that orbits the world’s origin when you rotate it is useless.

2. The order of multiplication

A CFrame is a position 𝐩 and a rotation R. Composing two applies the right one first, in the coordinates of the left one:

(𝐩,R)⋅(𝟎,Q)=(𝐩, RQ)

Multiplying the block’s CFrame on the right by a pure rotation Q leaves the position as it was, so the block turns around its centre, and it applies Q in the block’s own axes: CFrame.Angles(θ, 0, 0) on the right turns it around its own X axis, wherever that axis points in the world. Multiplying on the left,

(𝟎,Q)⋅(𝐩,R)=(Q𝐩, QR)

would rotate around the world’s axes and also swing the position around the origin. The tool does the first:

arrows.Adornee.CFrame = cf * CFrame.Angles(unpack(Vector(axis, angle)))

where cf is the CFrame saved at mouse-down. Like the move tool, the result is absolute, computed from the starting state and the total angle, so it doesn’t drift and doesn’t depend on how the mouse got there.

3. Snapping to the nearest

Vector(axis, angle) converts the angle to degrees, rounds it to the nearest multiple of the increment, converts it back to radians and puts it in the right slot of a table that unpack spreads into CFrame.Angles:

local function Round(num)
	return math.floor((num / value.Value) + 0.5) * value.Value
end
local Vector = function(axis, delta)
	local arg = rad(Round(deg(delta)))

	return axis == X_AXIS and ({arg, 0, 0})
		or axis == Y_AXIS and ({0, arg, 0})
		or axis == Z_AXIS and ({0, 0, arg});
end

With θ the angle in degrees and i the increment:

θs=i⌊θi+12⌋,|θs−θ|≤i2

The error is at most half an increment either way, so turning a ring forwards or backwards feels the same, unlike the ceil of the move tool. The increment goes up to 360°. An empty field means 15°, and “0” means 0.01°.

4. Three numbers to the server

On release, the tool sends RotateRep the block and its Orientation, three angles in degrees, and the server assigns Orientation. It doesn’t send the position and doesn’t need to: as section 2 shows, rotating didn’t change it, so the position the server has is still the right one.

Orientation is a set of Euler angles that Roblox applies in the order Y, X, Z:

R=Ry(ψ)Rx(ϕ)Rz(ρ)

Reading Orientation decomposes the block’s rotation into those three angles, and assigning it rebuilds the rotation from them. I checked the round trip on a block with an arbitrary rotation and the direction vectors came back within 4×10−6, which is float precision. Euler angles have configurations where the decomposition isn’t unique (when the X angle is ±90°, the Y and Z rotations turn around the same axis), but any of the possible triples rebuilds the same rotation, so that doesn’t break anything here. It’s the smallest message of the three transform tools, 3 numbers against the 12 of a CFrame, and it’s correct, which I don’t think I could have justified then.

5. The fields are three different rotations

The window shows the block’s Orientation in three fields. Typing an angle assigns that component and sends it to the server. Because of the order in which Orientation is applied, the three fields don’t rotate the block the way the rings do, and they don’t rotate it the same way as each other.

  • Changing Y changes the leftmost factor: Ry(ψ+δ)RxRz=Ry(δ)R. That’s a rotation around the world’s vertical axis.
  • Changing Z changes the rightmost factor: RyRxRz(ρ+δ)=RRz(δ). That’s a rotation around the block’s own Z axis, the same thing the Z ring does.
  • Changing X is in between: RyRx(ϕ+δ)Rz=RyRx(δ)Ry−1R, a rotation around the world’s X axis after it’s been turned by the yaw ψ. It matches the X ring only when ρ=0.

I checked the three with a block at an orientation of (10,40,25) and a change of 30°. The Y field matches a rotation around the world’s Y axis to within 10−7, the Z field matches the Z ring, and the X field differs from the X ring by up to 0.22 in its direction vectors while matching the rotation around the yawed X axis exactly. None of this is a bug in the code. It’s how Euler angles work, and the window shows the same three numbers as the Properties panel in Studio. But a player who rotates with the rings and then types into the fields is mixing two kinds of rotation without knowing it.

The clear buttons next to the fields send the server the orientation with that component at zero, without changing the block locally, so the change shows up when replication brings it back.

6. The first version

The version I wrote as juank had the same two functions, with Spanish names (redondear and vector), and the same multiplication. What changed in the rewrite is what’s around them. The old one was a Tool, connected the mouse events every time it was equipped, and fired the remote with the Orientation on every drag event. Its loop for reading the increment ran until sen == sen+1, a condition that is never true, as a way of looping forever.

7. Limitations

  • What snaps is the angle turned, not the final orientation. A block at 7° with an increment of 15° goes to 22°, 37° and so on, never to 15° or 30°.
  • The server accepts any orientation.
  • The fields and the rings are different operations, as section 5 explains, and nothing in the window says so.

8. What I took away

This is the tool I understood least when I wrote it, and the comment says as much. Looking at it now, all of it fits in one rule: multiplying on the right works in the block’s axes, and multiplying on the left works in the world’s. The same rule explains why the server only needs three numbers, and why the Y field turns the block around the world while the Z field turns it around itself.

This text by Mastedore. All rights reserved.