Hello,
In our game, we have a big spline along which most of the game happens. To easily place objects along that spline, I’d like to have a position handle that stays constant along the spline. But there seems to be issues with PositionHandle when it’s being moved externally in addition to the user input.
I tried two methods.
Method A “Handle Orientation”
I use a position handle with the orientation of the spline corresponding to the projection of that object on the spline. When I move the Z (blue) component of the handle, the object indeed moves along the spline but the components on the X and Y axis locally on the spline also move which should not be the case. I’m not sure if my math is wrong here or if it’s an issue with PositionHandle.
Method B “Handle Matrix”
Here, I set Handle.matrix to be oriented and offset so that all interactions with the handle are in “bezier spline space”. The problem here is that everytime I move along Z (blue), it moves more than expected. It almost seems like the value returned by PositionHandle is relative to the initial position of the handle instead of being relative to the one I input to PositionHandle.
In both cases, moving along X or Y works like a charm.
I extracted a sample code from my bigger project which exhibits my troubles. To try it, just add the file to a project, and open the “Tools/Bezier Gizmo” window. It will move the selected object to the position 0 on an hardcoded spline and movement can then be controlled either by inputing value in the window or moving the buggy handle.
using UnityEngine;
using UnityEditor;
public class SampleBezierGizmo : EditorWindow
{
enum MoveMethod
{
HandleMatrix,
HandleOrientation,
}
MoveMethod method;
float time;
Transform target;
Vector3 bezierPosition;
Quaternion bezierOrientation;
Vector2 localPosition;
Vector3 p0 = new Vector3(0, 0, 0);
Vector3 p1 = new Vector3(10, 0, 0);
Vector3 p2 = new Vector3(10, 0, 10);
Vector3 p3 = new Vector3(0, 0, 10);
Vector3 Bezier(float t)
{
float mt = 1.0f - t;
float t2 = t * t;
float mt2 = mt * mt;
float c0 = mt2 * mt;
float c1 = 3.0f * t * mt2;
float c2 = 3.0f * t2 * mt;
float c3 = t2 * t;
return c0 * p0 + c1 * p1 + c2 * p2 + c3 * p3;
}
Vector3 BezierDerivative(float t)
{
float mt = 1.0f - t;
float a = 3.0f * mt * mt;
float b = 6.0f * t * mt;
float c = 3.0f * t * t;
float c0 = -a;
float c1 = a - b;
float c2 = b - c;
float c3 = c;
return c0 * p0 + c1 * p1 + c2 * p2 + c3 * p3;
}
[MenuItem("Tools/Bezier Gizmo")]
static void Open()
{
SampleBezierGizmo window = GetWindow<SampleBezierGizmo>("Playspace Gizmo");
window.minSize = new Vector2(250, 60);
}
Tool prev;
void OnEnable()
{
SceneView.onSceneGUIDelegate += OnSceneGUI;
prev = Tools.current;
Tools.current = Tool.None;
}
void OnDisable()
{
SceneView.onSceneGUIDelegate -= OnSceneGUI;
Tools.current = prev;
}
void OnGUI()
{
method = (MoveMethod)EditorGUILayout.EnumPopup("Method", method);
EditorGUI.BeginChangeCheck();
Vector2 localMove = EditorGUILayout.Vector2Field("Position", localPosition) - localPosition;
float distanceMove = EditorGUILayout.FloatField("Time", time) - time;
if (EditorGUI.EndChangeCheck())
Move(localMove, distanceMove);
}
void OnSceneGUI(SceneView sv)
{
if (target != Selection.activeTransform)
Initialize();
switch (method)
{
case MoveMethod.HandleMatrix:
{
Handles.matrix = Matrix4x4.TRS(bezierPosition, bezierOrientation, Vector3.one);
EditorGUI.BeginChangeCheck();
Vector3 newLocalPosition = Handles.PositionHandle(localPosition, Quaternion.identity);
if (EditorGUI.EndChangeCheck())
{
Vector3 ofs = newLocalPosition - new Vector3(localPosition.x, localPosition.y, 0);
Move(ofs, ofs.z);
Debug.Log(localPosition.ToString() + ", " + newLocalPosition.ToString());
}
break;
}
case MoveMethod.HandleOrientation:
{
Vector3 worldPosition = bezierPosition + bezierOrientation * localPosition;
EditorGUI.BeginChangeCheck();
Vector3 newWorldPosition = Handles.PositionHandle(worldPosition, bezierOrientation);
if (EditorGUI.EndChangeCheck())
{
Vector3 newLocalPosition = Quaternion.Inverse(bezierOrientation) * (newWorldPosition - bezierPosition);
Move(new Vector2(newLocalPosition.x, newLocalPosition.y) - localPosition, newLocalPosition.z);
}
break;
}
}
Handles.matrix = Matrix4x4.TRS(bezierPosition, bezierOrientation, Vector3.one);
if (localPosition.y >= 0)
{
Handles.DrawLine(Vector3.zero, new Vector3(localPosition.x, 0, 0));
Handles.DrawLine(new Vector3(localPosition.x, 0, 0), localPosition);
}
else
{
Handles.DrawLine(Vector3.zero, new Vector3(0, localPosition.y, 0));
Handles.DrawLine(new Vector3(0, localPosition.y, 0), localPosition);
}
Handles.matrix = Matrix4x4.identity;
Handles.DrawBezier(p0, p3, p1, p2, Color.white, Texture2D.whiteTexture, 3);
}
void Move(Vector2 localOffset, float distanceOffset)
{
bezierPosition = Bezier(time);
Vector3 derivative = BezierDerivative(time);
bezierOrientation = Quaternion.LookRotation(derivative.normalized);
time += distanceOffset / derivative.magnitude;
localPosition += localOffset;
Undo.RecordObject(Selection.activeTransform, "Moving selected transform");
Selection.activeTransform.position = bezierPosition + bezierOrientation * localPosition;
EditorUtility.SetDirty(Selection.activeTransform);
Repaint();
}
void Initialize()
{
localPosition = Vector2.zero;
time = 0;
bezierPosition = Bezier(time);
Vector3 derivative = BezierDerivative(time);
bezierOrientation = Quaternion.LookRotation(derivative.normalized);
target = Selection.activeTransform;
Repaint();
}
}
Any idea of what I am doing wrong ?