How can I convert rotation where the Y/Z axes are flipped to Roblox rotation?

I am working on an FBX animation importer, and the rotation values stored in the files treat Z as the vertical axis and Y as the depth axis, instead of the other way around like it is on Roblox. These rotations are in local space (relative to the parent node) rather than global rotation. If I import the rotations as-is, I get malformed results because the Y/Z axes are flipped:

Original:

How can I convert this to proper Roblox rotation? I have access to both the rx,ry,rz rotation values (in wrong axes) and the CFrame matrix (in wrong axes), and can do the rotation in either Roblox after the KeyframeSequence has already been generated or before generating it in Python. I would prefer before so the KeyframeSequence is usable as-is. This is not a Blender plugin, so I don’t have access to libraries like bpy or mathutils.

Resources:

FBX File: RPG-Character@Unarmed-Attack-Kick-L1.FBX (481.0 KB)
Generated KeyframeSequence with wrong axes of rotation: KeyframeSequence.rbxmx (1.5 MB)

Code to visualize KeyframeSequence:

local kfs = ...
local kf = kfs.Keyframe_0

function scaleCFrame(cf, factor)
	return cf - cf.p + cf.p*factor
end

function displayPose(pose, parentNode)
	local baseCFrame = parentNode and parentNode.CFrame or CFrame.new()
	
	local part = Instance.new("Part")
	part.CFrame = baseCFrame * pose.CFrame
	part.Name = pose.Name
	part.Size = Vector3.new(1,1,1)
	part.Transparency = 1
	part.Anchored = true
	part.CanCollide = false
	part.Parent = workspace
	
	Instance.new("Attachment", part)
	
	local sphere = Instance.new("SphereHandleAdornment")
	sphere.Radius = 0.5
	sphere.Color3 = Color3.new(0.1,0.25,0.75)
	sphere.Adornee = part
	sphere.Parent = part
	
	if parentNode then
		local beam = Instance.new("Beam")
		beam.Attachment0 = part.Attachment
		beam.Attachment1 = parentNode.Attachment
		beam.Parent = part
		beam.CurveSize0 = 1
		beam.CurveSize1 = 1
		beam.FaceCamera = true
	end
	
	for _,v in pairs(pose:GetChildren()) do
		displayPose(v, part)
	end
end

for _,v in pairs(kf:GetChildren()) do
	displayPose(v)
end
2 Likes

If you are using blender then you can likely tell the plugin exporting the FBX file how the axis should be orientated 3d view - Is it possible to make Blender a Y-up world? - Blender Stack Exchange.

If this is not an option then you can flip the y/z axis with a matrix multiplication with the following matrix

1 0 0
0 0 1
0 1 0

although do note that you may need to negate the y/z axis which I think you can do with

1  0  0
0  0 -1
0 -1  0

doing this in Roblox with your visualisation script would look something like this

local kfs = workspace.ImportedKeyframeSequence
local kf = kfs.Keyframe_0

local flipCF = CFrame.new(0,0,0,
						  1,0,0,
						  0,0,-1,
						  0,-1,0)

function scaleCFrame(cf, factor)
	return cf - cf.p + cf.p*factor
end

function convCFrame(cf)
	return cf * flipCF
end

function displayPose(pose, parentNode)
	local baseCFrame = parentNode and parentNode.CFrame or CFrame.new()
	
	local part = Instance.new("Part")
	part.CFrame = baseCFrame * convCFrame(pose.CFrame)
	part.Name = pose.Name
	part.Size = Vector3.new(1,1,1)
	part.Transparency = 1
	part.Anchored = true
	part.CanCollide = false
	part.Parent = workspace
	
	Instance.new("Attachment", part)
	
	local sphere = Instance.new("SphereHandleAdornment")
	sphere.Radius = 0.5
	sphere.Color3 = Color3.new(0.1,0.25,0.75)
	sphere.Adornee = part
	sphere.Parent = part
	
	if parentNode then
		local beam = Instance.new("Beam")
		beam.Attachment0 = part.Attachment
		beam.Attachment1 = parentNode.Attachment
		beam.Parent = part
		beam.CurveSize0 = 1
		beam.CurveSize1 = 1
		beam.FaceCamera = true
	end
	
	for _,v in pairs(pose:GetChildren()) do
		displayPose(v, part)
	end
end

for _,v in pairs(kf:GetChildren()) do
	displayPose(v)
end

This does look a little funny to me though. I’m not sure if my transformation is wrong or if your visualisation isn’t behaving the way I expect. Either way I hope this helps point you in the right direction.

2 Likes

Yeah, this is not an option unfortunately. I’m writing a plugin to import FBX animations from third-party stores (e.g. Unity Asset Store) into Roblox, so Blender isn’t involved at all in the conversion process.

Does the CFrame need to be converted in world space? Each of these CFrames is relative to the previous node.

1 Like

No. This is easier if you consider every matrix as what it is, just a transformation. World/object space only matters in the context of what you are using it in but here we only care about switching it y/z axis not really about what it is being used for later (as if they are all converted correctly they all should just work later).

Edit: Upon further looking this may be more complicated than just a matrix multiplication. There seems to be some confusion over this here math - Changing a matrix from right-handed to left-handed coordinate system - Stack Overflow. Would you mind dm’ing me your Python code so I can see what kind of info you have from the FBX file available to you?

I’ve attached everything just out of courtesy for anyone who wants to do granular manipulation of FBX files in the future, but I’ll summarize the important information since it is a lot to look through.

Resources

Conversion of binary FBX file to XML for human readablity: fbx.xml (839.3 KB)

Zip of the project (converts FBX animation to KeyframeSequence rbxmx in blender axes): FBX.zip - Google Drive

FBX file format specification:

Summary

The FBX file provides position and rotation as X, Y, and Z deltas from the previous node. Both position/rotation are stored as d|X, d|Y, and d|Z – they are all tied together by the Connections section by ID (first number in data field for objects). So we have X,Y,Z position and X,Y,Z rotation (degrees) in object space relative to the previous node.

Rotation is in degrees, but the Python script converts it to radians, then converts to CFrame, and finally stores in a .rbxmx KeyframeSequence. I’ve tweaked the scripts so Python gives Roblox the raw x,y,z rotations instead of CFrame and lets Roblox do the conversion to CFrame with CFrame.Angles(rx,ry,rz), and the results are the same, so the conversion from rx, ry, and rz to CFrame in Python is correct.

1 Like

Wow this is turning into a surprisingly horrible problem! D: So because FBX is a proper file format it does not force you to conform to any handed system or any orientation of axis. This means you could be given a right-handed z-up coordinate system (which is what we would expect from something like blender) but it could be left-handed z-up or right-handed y-up or whatever it could even be x-up! You will need to parse this information to be able to handle every file as you will need to convert to roblox’s left-handed y-up coordinate system. BLEH!

I’m not experienced enough with FBX files to even understand what is being used in the example file you gave. In the XML file we have the following variables

<Node name='P' data=('UpAxis', 'int', 'Integer', '', 1) />
<Node name='P' data=('UpAxisSign', 'int', 'Integer', '', 1) />
<Node name='P' data=('FrontAxis', 'int', 'Integer', '', 2) />
<Node name='P' data=('FrontAxisSign', 'int', 'Integer', '', 1) />
<Node name='P' data=('CoordAxis', 'int', 'Integer', '', 0) />
<Node name='P' data=('CoordAxisSign', 'int', 'Integer', '', 1) />
<Node name='P' data=('OriginalUpAxis', 'int', 'Integer', '', 2) />
<Node name='P' data=('OriginalUpAxisSign', 'int', 'Integer', '', 1) />

Looking at the spec Help
UpAxis seems to be x=1,y=2,z=3 (although it would make more sense for x=0,y=1,z=2?)
FrontAxis is dependant on what the UpAxis is (I’m not entirely sure how this parity works)
CoordAxis sets if the system is right-handed or left-handed (0=right handed 1=left handed?)

Then we also have OriginalUpAxis? Does this mean a conversion is done before being put into the FBX file? Why do we need to know this and is this important?

So from this I understand this file to be y-up z-back (towards camera) and is right handed (and the model used to be z-up?)? Although I’m entirely uncertain. If you would be able to clarify this then that would be very helpful!

Once you know what you need to convert from you can start to do this properly. Here is a useful post on how to convert left/right handed euler angles (3d - Right-Handed Euler Angles XYZ to Left-Handed Euler Angles XYZ - Stack Overflow) and then converting between y/z up should hopefully be as simple as swapping y/z values (although remember you may still need to negate z values as you can convert from +z being away from the camera to -z being away from the camera and then that z could have also been y before so be careful!).

1 Like