FBX coordinate system confusion?

When importing an FBX from LightWave into Unity, the model comes in “backwards”, even though both LightWave and Unity have left hand coordinate systems with Y as up and Z as forward.

When importing the same FBX into left hand Cinema4D, the Z axis is not reversed, so why in Unity?

I am wondering if it is this due to some compensation-flip/rotation that is done on import of an FBX in Unity, so that it comes in with Z as forward when exported from Maya, which is a right hand coordinate system?

As a fellow lightwaver I share your pain :stuck_out_tongue:

What version of LW are you using?
Are you using LW 11’s “built in” Unity auto-import stuff?
What FBX plugin are you using for export? (if you’re using a 3rd party one)

Hi there. I am using the built in FBX exporter in LightWave 11 as it is by far the best one available to us LightWave people. The thousand dollar question is why Unity decides to play nice with Maya’s right hand coordinate system and be silly with us who are using the same left hand coordinate system as Unity. It would be nice to hear from some Unity peeps on the issue if my assumption is correct, that it is a rotational compensation done on import to play nice with Maya?

It’s hard to say why this is happening without proper investigation, but my guess is that there is something wrong with the way LW writes its FBX files (there are more known problems). The most likely reason why it works in other packages: Unity converts everything into single-pivot system (for performance reasons), and other modeling packages support multi-pivot systems (i.e. pre/post transforms), so the problem is not visible in other packages.

Please submit your model and report as a bug and I’ll try to investigate it later… Please export to FBX in ASCII format. Thanks :slight_smile:

I seriously doubt it to be a LightWave bug, since all other applications tested so far for export (3DS Max, Blender and LightWave, native files as well as FBX and OBJ) yield the same result. Only in from Maya does the 3D application’s forward axis come in as forward in Unity. I only have demo versions of Cheetah and Cinema4D, so I can not test their FBX exporters.

I have attached a new screenshot showing all the axis/orientations being reversed, apart from the ones from Maya which come in correctly. I have also attached the project folder for all to dissect.

Could users of C4D, Cheetah, XSI and Houdini try to export an arrow like this, using the application’s default forward/depth axis as the arrow’s forward axis too?


1129534–42743–$ZAxisConfusion.zip (437 KB)

Bump. Anyone?

How come files from left hand coordinate system 3D apps come in with a reversed forward axis in Unity, while right hand axis Maya does not? Can someone at Unity Tech. who “was there” at the beginning (OTEE), confirm or refute that Unity does this to play nice with Maya? Any and all information on this would be appreciated as I can report it directly to the LightWave FBX developer at NewTek.

If anything it should have been the other way around with right-hand coordinate system imports having their “sideways” axis flipped, while maintaining their forward axis as forward in Unity (+Z)

Also, could people with C4D/Cheetah/XSI/Houdini export a similar arrow, pointing down the positive Z axis to see how those apps fare when opening the FBX in Unity?

Paulius, did you download the zip file and check out the Blender file? Forward axis in Blender != forward axis in Unity? Why is it so?

I gots mi’self an ASCII FBX exported from C4D with Z as forward. When imported in Unity, its axis/pivot is reversed as well.

1146095–43526–$ArrowFromCinema4DFBX-ASCII.fbx.zip (4.9 KB)

More “evidence” to my theory on why forward Z axis in right hand coordinate system Maya == forward Z axis in left hand coordinate system Unity.

From the Wayback Machine (2005/2006), we can see that Maya is on top of the list:

And on the Maya info page we have this as well:

I see that Cinema4D was supported with automatic conversion of .c4d files as well, but my gut feeling is still that the focus was to ensure “play nice with Maya” :slight_smile:

So in essence I would like to see/hear a yes or no to the theory that Unity is playing nice with Maya due to legacy decisions made back in the days when Unity was Mac only.

If this is the case, then it is not a bug (neither in Unity or any of the non-Maya 3D apps), but a “by design” decision. I can then take the information straight to the LightWave FBX guy and see if we can have a “Z-axis rotation/flip” setting on exporting from LightWave to compensate for this rotation/flip we see in Unity. It would be nice to animate things “forward” in the 3D app, and have it also move forward in Unity. :slight_smile:

No one at Unity Tech. can answer this?

This could be a good idea in the interim, we are on the case though, currently in the process of doing tests with lightwave 11.5.

Thats good to hear :slight_smile:

Any findings? Also, as you can see from the tests above, it is not really a LightWave related issue. 3DS Max, Blender, Cinema4D and so on has the same problem.

I wrote a model postprocessor that automatically fixes the issue. However I’m not sure about how it deals with animation.

DELETED

Original scene in Cinema4D

Before postprocessing

After postprocessing

The code from the previous post was incorrect. It didn’t support object hierarchies. This one seems to work fine:

using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;

public class Cinema4DModelPostprocessor : AssetPostprocessor
{
	private readonly Quaternion rotation = Quaternion.Euler(0, 180, 0);

	private void OnPostprocessModel(GameObject go)
	{
		var gameObjects = new List<GameObject>();
		var worldPositions = new List<Vector3>();
		var worldRotations = new List<Quaternion>();

		var queue = new Queue<GameObject>();
		queue.Enqueue(go);

		while (queue.Any())
		{
			var item = queue.Dequeue();

			gameObjects.Add(item);
			worldPositions.Add(item.transform.position);
			worldRotations.Add(item.transform.rotation);

			foreach (Transform childTransform in item.transform)
			{
				queue.Enqueue(childTransform.gameObject);
			}
		}

		for (int i = 0; i < gameObjects.Count; i++)
		{
			ApplyTransformRotation(gameObjects[i], worldPositions[i], worldRotations[i]);
			ApplyGeometryRotation(gameObjects[i]);
		}

		go.transform.rotation = Quaternion.identity;

	}

	private void ApplyTransformRotation(GameObject go, Vector3 position, Quaternion initialRotation)
	{
		go.transform.position = position;
		go.transform.rotation = initialRotation * rotation;
	}

	private void ApplyGeometryRotation(GameObject go)
	{
		var meshFilter = go.GetComponent<MeshFilter>();
		if (meshFilter == null)
		{
			return;
		}
		var mesh = meshFilter.sharedMesh;
		var vertices = mesh.vertices;
		for (int i = 0; i < vertices.Length; i++)
		{
			vertices[i] = rotation * vertices[i];
		}
		mesh.vertices = vertices;

		var normals = mesh.normals;
		for (int i = 0; i < normals.Length; i++)
		{
			normals[i] = rotation * normals[i];
		}
		mesh.normals = normals;
		meshFilter.sharedMesh.RecalculateBounds();

		mesh.name = go.name;
	}
}

Interesting. I will have to give that a go. But yes, animation could be a bit of a challenge.

I checked how keyframe animation works and, as I suspected, it didn’t work correctly.

Cinema 4D:
1432096--76164--$c.gif

Unity:
1432096--76165--$u.gif

Unfortunately, I see no way to access the keyframe data to modify it.

I have just found AnimationUtility class that gives the access to the keyframes. I believe, I can fix animations too.

I’m not sure, but it seems I’ve managed to fix animation. Guys, please, could you test this script on your models/scenes? I think, it should work both for Cinema4D and LightWave since both of them use left-handed coordinate system with Y up.

Cinema4D <----> Unity
1434436--76517--$cinema.gif 1434436--76518--$unity.gif

using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;

public class Cinema4DModelPostprocessor : AssetPostprocessor
{
	private readonly Quaternion rotation = Quaternion.Euler(0, 180, 0);

	private void OnPostprocessModel(GameObject go)
	{
		var gameObjects = new List<GameObject>();
		var worldPositions = new List<Vector3>();
		var worldRotations = new List<Quaternion>();
		var queue = new Queue<GameObject>();
		queue.Enqueue(go);
		while (queue.Any())
		{
			var item = queue.Dequeue();
			gameObjects.Add(item);
			worldPositions.Add(item.transform.position);
			worldRotations.Add(item.transform.rotation);
			foreach (Transform childTransform in item.transform)
			{
				queue.Enqueue(childTransform.gameObject);
			}
		}

		for (int i = 0; i < gameObjects.Count; i++)
		{
			ApplyTransformRotation(gameObjects[i], worldPositions[i], worldRotations[i]);
			ApplyGeometryRotation(gameObjects[i]);
			//if (gameObjects[i] != go)
			{
				FixAnimation(go, gameObjects[i]);
			}
		}

		go.transform.rotation = Quaternion.identity;
	}

	private void FixAnimation(GameObject root, GameObject go)
	{
		var clips = AnimationUtility.GetAnimationClips(root);
		foreach (var clip in clips)
		{
			var curves = FindRotationCurves(clip, GetRelativePath(root, go));
			if (curves != null)
			{
				InvertCurve(curves[0].curve);
				InvertCurve(curves[2].curve);

				clip.SetCurve(curves[0].path, curves[0].type, curves[0].propertyName, curves[0].curve);
				clip.SetCurve(curves[2].path, curves[2].type, curves[2].propertyName, curves[2].curve);
			}

			curves = FindPositionCurves(clip, GetRelativePath(root, go));
			if (curves != null)
			{
				InvertCurve(curves[0].curve);
				InvertCurve(curves[2].curve);

				clip.SetCurve(curves[0].path, curves[0].type, curves[0].propertyName, curves[0].curve);
				clip.SetCurve(curves[2].path, curves[2].type, curves[2].propertyName, curves[2].curve);
			}
		}
	}

	private string GetRelativePath(GameObject root, GameObject child)
	{
		string path = "";
		var transform = child.transform;
		while (transform.gameObject != root)
		{
			if (path == "")
			{
				path = transform.name;
			}
			else
			{
				path = transform.name + "/" + path;
			}
			transform = transform.parent;
		}
		return path;
	}

	private AnimationClipCurveData[] FindRotationCurves(AnimationClip clip, string path)
	{
		AnimationClipCurveData xCurveData = null, yCurveData = null, zCurveData = null, wCurveData = null;

		var curves = AnimationUtility.GetAllCurves(clip, true);
		foreach (var curveData in curves)
		{
			if (curveData.path != path)
			{
				continue;
			}

			switch (curveData.propertyName)
			{
				case "m_LocalRotation.x":
					xCurveData = curveData;
					break;
				case "m_LocalRotation.y":
					yCurveData = curveData;
					break;
				case "m_LocalRotation.z":
					zCurveData = curveData;
					break;
				case "m_LocalRotation.w":
					wCurveData = curveData;
					break;
			}

			if (xCurveData != null  yCurveData != null  zCurveData != null  wCurveData != null)
			{
				return new[] { xCurveData, yCurveData, zCurveData, wCurveData };
			}
		}

		return null;
	}

	private AnimationClipCurveData[] FindPositionCurves(AnimationClip clip, string path)
	{
		AnimationClipCurveData xCurveData = null, yCurveData = null, zCurveData = null;

		var curves = AnimationUtility.GetAllCurves(clip, true);
		foreach (var curveData in curves)
		{
			if (curveData.path != path)
			{
				continue;
			}

			switch (curveData.propertyName)
			{
				case "m_LocalPosition.x":
					xCurveData = curveData;
					break;
				case "m_LocalPosition.y":
					yCurveData = curveData;
					break;
				case "m_LocalPosition.z":
					zCurveData = curveData;
					break;
			}

			if (xCurveData != null  yCurveData != null  zCurveData != null)
			{
				return new[] { xCurveData, yCurveData, zCurveData };
			}
		}

		return null;
	}

	private void InvertCurve(AnimationCurve curve)
	{
		var keyframes = curve.keys;

		for (int i = 0; i < keyframes.Length; i++)
		{
			var keyframe = keyframes[i];
			keyframe.value = -keyframe.value;
			keyframes[i] = keyframe;
		}

		curve.keys = keyframes;
	}

	private void ApplyTransformRotation(GameObject go, Vector3 position, Quaternion initialRotation)
	{
		go.transform.position = position;
		go.transform.rotation = initialRotation * rotation;
	}

	private void ApplyGeometryRotation(GameObject go)
	{
		var meshFilter = go.GetComponent<MeshFilter>();
		if (meshFilter == null)
		{
			return;
		}

		var mesh = meshFilter.sharedMesh;
		var vertices = mesh.vertices;
		for (int i = 0; i < vertices.Length; i++)
		{
			vertices[i] = rotation * vertices[i];
		}

		mesh.vertices = vertices;
		var normals = mesh.normals;
		for (int i = 0; i < normals.Length; i++)
		{
			normals[i] = rotation * normals[i];
		}

		mesh.normals = normals;
		meshFilter.sharedMesh.RecalculateBounds();
		mesh.name = go.name;
	}
}

1434436–76519–$Cinema4D_Fix.unitypackage (163 KB)

Any plans to fix this (most interested in C4D) in Unity 5?