Why Rotating Y Axis Rotates Globally Instead Of Locally

I’m trying to make a placement system with rotation feature, when I rotate to Y axis at first it works normally, but when I rotate the item on Z axis once (90 degrees) and rotate Y axis again, it rotates Y in global Axis, I want it to rotate on local Y axis

(Y Axis rotation is showed as the fast rotating ones, I apologize for not having a text on screen to indicate which rotation is Y)

this is the script in charge for the cframe (module script)

		self.RunService = RS.Heartbeat:Connect(function()
			local LPlr = game:GetService("Players").LocalPlayer
			local Mouse = LPlr:GetMouse()
			Mouse.TargetFilter = workspace.Placing
			local MousePosition = Mouse.Hit.Position

			local PlacingPosition = Vector3.new(Round(MousePosition.X, self.Grid), Round(MousePosition.Y, self.Grid), Round(MousePosition.Z, self.Grid))
			local PlacingCFrame = (CFrame.new(PlacingPosition) * CFrame.new(self.CFrameOffset)) * CFrame.fromEulerAngles(Rad(self.Rotation))
			
			self.PrototypePosition = PlacingPosition
			self.Prototype.PrimaryPart.CFrame = (PlacingCFrame)
		end)

and this one is the local script

local ObjectProfile = nil
local ObjectRotationX = 0
local ObjectRotationY = 0
local ObjectRotationZ = 0

local TweenRotationX = Instance.new("NumberValue")
local TweenRotationY = Instance.new("NumberValue")
local TweenRotationZ = Instance.new("NumberValue")

UIS.InputBegan:Connect(function(Input, GameProcess)
	if GameProcess then return end
	if Input.KeyCode == Enum.KeyCode.E then
		if ObjectProfile == nil then
			ObjectProfile = GridPlacingModule.New({DataName = "Banana", ModelName = "Banana", DisplayName = "Banana", CFrameOffset = Vector3.new(0, 4.25, 0)})
			ObjectRotationX = ObjectProfile.Rotation.Y
			ObjectRotationY = ObjectProfile.Rotation.Y
			ObjectRotationZ = ObjectProfile.Rotation.Z
			
			TweenRotationX.Value = ObjectProfile.Rotation.Y
			TweenRotationY.Value = ObjectProfile.Rotation.Y
			TweenRotationZ.Value = ObjectProfile.Rotation.Z
		else
			ObjectProfile:Stop()
			ObjectProfile = nil
		end
	elseif Input.KeyCode == Enum.KeyCode.R then
		if ObjectProfile ~= nil then
			ObjectRotationY += 90
			TS:Create(TweenRotationY, TweenInfo.new(0.2, Enum.EasingStyle.Back, Enum.EasingDirection.Out), {Value = ObjectRotationY}):Play()
		end
	elseif Input.KeyCode == Enum.KeyCode.T then
		if ObjectProfile ~= nil then
			if ObjectRotationY / 180 == math.round(ObjectRotationY / 180) then
				ObjectRotationX += 90
				TS:Create(TweenRotationX, TweenInfo.new(0.2, Enum.EasingStyle.Back, Enum.EasingDirection.Out), {Value = ObjectRotationX}):Play()
			else
				ObjectRotationZ += 90
				TS:Create(TweenRotationZ, TweenInfo.new(0.2, Enum.EasingStyle.Back, Enum.EasingDirection.Out), {Value = ObjectRotationZ}):Play()
			end
		end
	end
end)

TweenRotationX:GetPropertyChangedSignal("Value"):Connect(function()
	if ObjectProfile ~= nil then
		local Val = TweenRotationX.Value
		print("X - "..tostring(Val))
		ObjectProfile.Rotation = Vector3.new(Val, ObjectProfile.Rotation.Y, ObjectProfile.Rotation.Z)
	end
end)
TweenRotationY:GetPropertyChangedSignal("Value"):Connect(function()
	if ObjectProfile ~= nil then
		local Val = TweenRotationY.Value
		print("Y - "..tostring(Val))
		ObjectProfile.Rotation = Vector3.new(ObjectProfile.Rotation.X, Val, ObjectProfile.Rotation.Z)
	end
end)
TweenRotationZ:GetPropertyChangedSignal("Value"):Connect(function()
	if ObjectProfile ~= nil then
		local Val = TweenRotationZ.Value
		print("Z - "..tostring(Val))
		ObjectProfile.Rotation = Vector3.new(ObjectProfile.Rotation.X, ObjectProfile.Rotation.Y, Val)
	end
end)
2 Likes

Try changing the EulerAngles Order.

CFrames operate on Quaternions, meaning they have a 4th W rotation value representative of an Axis to rotate along, it’s kind of complicated.

All you need to know is that the CFrame.FromEulerAngles() Translates Euler Angles, your typical X,Y,Z Values to a Quaternion value. However because Euler Angles are not deterministic unless the order they’re applied in is specified, there are other functions such as: CFrame.FromEulerAnglesXYZ() or CFrame:FromEulerAnglesYXZ().

TL;DR:
Try CFrame:FromEulerAnglesYXZ() To change the order and hopefully get it right.

If that doesn’t work you can use CFrame:FromEulerAngles() and try different Enum.RotationOrder parameters until you get it right. If nothing works the problem is elsewhere.

i tried these, and it doesnt solve the problem but i appreciate the effort

one thing ive noticed is that even when im only rotating the item on X axis (as you can see on the bottom left that increased to over 1,000) the other axis (like Y) also changes and im really confused

This most likely occurs due to gimbal lock and how CFrames work, although I could be 100% wrong about this. Gimbal lock occurs when rotating, say 90 degrees either two axes would rotate in the same axis instead of rotating locally. You would have to use CFrame manipulation to rotate an object, since adding two CFrames manually will get you gimbal lock. This especially happens when an object is only manipulation either rotation or orientation by Vector3 values alone. The binary operation under CFrame multiplication from the right of an object’s CFrame will rotate it locally instead of globally by simple CFrame knowledge. (not trying to use group theory/abstract algebra context here)
Refer to these topics for guidance:

Wrong or not wrong, helping or not helping; I am still learning CFrame mathematics and I am not a professional algebraist.

I think it is because how to rotation is being handled

I assume that self.Rotation from the below line is referring to ObjectProfile.Rotation (from the local script)

local PlacingCFrame = (CFrame.new(PlacingPosition) * CFrame.new(self.CFrameOffset)) * CFrame.fromEulerAngles(Rad(self.Rotation))

If this is the case, then the rotation will inherently be relative to global axis. This is because ObjectProfile.Rotation is just a “free floating” Vector3 that is not relative to anything. So, in the module script where the rotation is applied, it sets the rotation globally. (specifically, set position to PlacingPosition → add self.CFrameOffset → set rotation to self.Rotation

In order to apply the rotations locally, I believe you would have to somehow reference the existing CFrame and then based on that you can rotate it how you want.

For example, one way that I thought of doing this (using the existing infrastructure you have) is to create a local CFrame and then rotate that CFrame using local space. You would then use that CFrame in the module script.

Ex.

at the start of your script

--define CFrame
local rotationCFrame = Instance.new("CFrameValue")
rotationCFrame.Value = CFrame.Angles(0, 0, 0)

rotate CFrame locally when rotate key (ex. “R”) pressed

[...]
elseif Input.KeyCode == Enum.KeyCode.R then
    if ObjectProfile ~= nil then
			TS:Create(rotationCFrame, TweenInfo.new(0.2, Enum.EasingStyle.Back, Enum.EasingDirection.Out), {Value = rotationCFrame.Value * CFrame.Angles(0, math.rad(90), 0)}):Play()
    end
[...]

modify listener

rotationCFrame:GetPropertyChangedSignal("Value"):Connect(function()
    if ObjectProfile ~= nil then
        ObjectProfile.Rotation = rotationCFrame -- note that the data type of ObjectProfile.Rotation is now a CFrame instead of a Vector3
    end
end)

modify module script

local PlacingCFrame = (CFrame.new(PlacingPosition) * CFrame.new(self.CFrameOffset)) * self.Rotation

Let me know if this helps!

I gave up and decided to only allow rotation to 1 axis, but I will keep these information for future projects, appreciated