I'm working on a bezier curve implementation in Unity. Until recently, I had no need for roll (the objects that followed the curve always stood upright) but I've since added a roll value to each control point (0-360°, the same way roll is stored in Blender). I'm calculating rotation using global up, which results in nasty twisting when going up/around loops. This is equivalent to Blender's Z-Up twist method; exporting my test track model using Z-Up demonstrates the exact same problem:
Any object that follows the track flips around when it reaches these ugly twists/control points. Blender also has a "Minimum" twist method that produces much better results:
Does anyone know what Blender's "Minimum" twist method is/how to reproduce it in Unity? I'd like to have roll calculated consistently between Blender and Unity.
Attempted Solution #1
I've attempted to solve this issue by computing normals at each control point using the normal of the previous control point. However, my implementation is either not correct or there is something more I need to do to handle loops.
Here is the relevant code:
public Vector3 ComputeBinormal(Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, Vector3 up, float t)
{
Vector3 tangent = ComputeTangent(p0, p1, p2, p3, t);
return Vector3.Cross(up, tangent).normalized;
}
public Vector3 ComputeNormal(Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, Vector3 up, float t)
{
Vector3 tangent = ComputeTangent(p0, p1, p2, p3, t);
Vector3 binormal = ComputeBinormal(p0, p1, p2, p3, up, t);
return Vector3.Cross(tangent, binormal).normalized;
}
public Vector3 GetNormal(float t)
{
int index = GetCurveIndex(t);
return transform.TransformPoint(ComputeNormal(points[index], points[index + 1], points[index + 2], points[index + 3], Vector3.up, GetCurveValue(t))) - transform.position;
}
public void ResetNormals()
{
normals = new Vector3[(points.Length - 1) / 3 + 1];
normals[0] = Vector3.up;
Vector3 tangent = ComputeTangent(points[0], points[1], points[2], points[3], 0f);
Vector3 binormal = Vector3.Cross(tangent, Vector3.up).normalized;
for (int i = 1, j = 0; i < normals.Length; i++, j += 3)
{
normals[i] = ComputeNormal(points[j], points[j + 1], points[j + 2], points[j + 3], normals[i - 1], 0f);
}
}
public Quaternion GetRotation(float t)
{
// backwards compatibility so you can upgrade projects
if (rolls.Length != modes.Length)
{
return Quaternion.LookRotation(GetVelocity(t));
}
else
{
if (normals.Length != modes.Length)
{
ResetNormals();
}
Quaternion roll1 = Quaternion.Euler(0f, 0f, GetControlPointRoll(GetCurveIndex(t)));
Quaternion roll2 = Quaternion.Euler(0f, 0f, GetControlPointRoll(GetCurveIndex(t) + 4));
return Quaternion.LookRotation(GetVelocity(t), GetControlPointNormal(GetCurveIndex(t))) * Quaternion.Lerp(roll1, roll2, GetCurveValue(t));
}
}
Attempted Solution #2
It's clear that computing normals for just the control points is not sufficient; the normal has to be computed per point on the curve (i.e. each frame a walker moves along the curve). In that case, how would I start a walker at any arbitrary point on the curve, not just at zero?