6
\$\begingroup\$

I have created a cubic bezier curve using the Unity editor. How can I preserve the bezier curve symmetry when moving the end points or control points/anchor points? Please see my code below and the image:

enter image description here

UPDATE

Generally, when one of the end points (A or B) is moved, the control points (cp1 and cp2) will also move to correct the distance between them preserving the symmetry. Likewise when one control point is moved, only the other control point will move mirroring it’s movement (along/about the line of symmetry) to preserve the symmetry.

Bezier:

public static class Bezier {

public static Vector3 GetPoint (Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float t) {
    t = Mathf.Clamp01(t);
    float oneMinusT = 1f - t;
    return
        oneMinusT * oneMinusT * oneMinusT * p0 +
        3f * oneMinusT * oneMinusT * t * p1 +
        3f * oneMinusT * t * t * p2 +
        t * t * t * p3;
}

public static Vector3 GetFirstDerivative (Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float t) {
    t = Mathf.Clamp01(t);
    float oneMinusT = 1f - t;
    return
        3f * oneMinusT * oneMinusT * (p1 - p0) +
        6f * oneMinusT * t * (p2 - p1) +
        3f * t * t * (p3 - p2);
}
}

BezierCurve:

[RequireComponent(typeof(LineRenderer))]
public class BezierCurve : MonoBehaviour {

public Vector3[] points;
LineRenderer lr;
public int numPoints = 49;
bool controlPointsChanged = false;

bool isMoving = false;

public void Reset () {
    points = new Vector3[] {
        new Vector3(1f, 0f, 0f),
        new Vector3(2f, 0f, 0f),
        new Vector3(3f, 0f, 0f),
        new Vector3(4f, 0f, 0f)
    };
}

void Start()    {

    lr = GetComponent<LineRenderer> ();
    lr.positionCount = 0;
    DrawBezierCurve ();

}
public Vector3 GetPoint (float t) {
    return transform.TransformPoint(Bezier.GetPoint(points[0], points[1], points[2], points[3], t));
}

public void DrawBezierCurve ()  {
    lr = GetComponent<LineRenderer> ();
    lr.positionCount = 1;
    lr.SetPosition(0, points[0]);

    for (int i = 1; i < numPoints+1; i++) {
        float t = i / (float)numPoints;
        lr.positionCount = i+1;
        lr.SetPosition(i, GetPoint(t));
    }
}

public Vector3 GetVelocity (float t) {
    return transform.TransformPoint(
        Bezier.GetFirstDerivative(points[0], points[1], points[2], points[3], t)) - transform.position;
}

public Vector3 GetDirection (float t) {
    return GetVelocity(t).normalized;
}
}

BezierCurveEditor:

[CustomEditor(typeof(BezierCurve))]
public class BezierCurveEditor : Editor {

private BezierCurve curve;
private Transform handleTransform;
private Quaternion handleRotation;

private const int lineSteps = 10;

private const float directionScale = 0.5f;

private void OnSceneGUI () {
    curve = target as BezierCurve;
    handleTransform = curve.transform;
    handleRotation = Tools.pivotRotation == PivotRotation.Local ?
        handleTransform.rotation : Quaternion.identity;

    Vector3 p0 = ShowPoint(0);
    Vector3 p1 = ShowPoint(1);
    Vector3 p2 = ShowPoint(2);
    Vector3 p3 = ShowPoint(3);

    Handles.color = Color.gray;
    Handles.DrawLine(p0, p1);
    Handles.DrawLine(p2, p3);
    Handles.DrawBezier(p0, p3, p1, p2, Color.white, null, 2f);

    curve.DrawBezierCurve ();

    if (GUI.changed) {
        curve.DrawBezierCurve ();
        EditorUtility.SetDirty( curve );
        Repaint();
    }

}


private void ShowDirections () {
    Handles.color = Color.green;
    Vector3 point = curve.GetPoint(0f);
    Handles.DrawLine(point, point + curve.GetDirection(0f) * directionScale);
    for (int i = 1; i <= lineSteps; i++) {
        point = curve.GetPoint(i / (float)lineSteps);
        Handles.DrawLine(point, point + curve.GetDirection(i / (float)lineSteps) * directionScale);
    }
}

private Vector3 ShowPoint (int index) {
    Vector3 point = handleTransform.TransformPoint(curve.points[index]);
    EditorGUI.BeginChangeCheck();
    point = Handles.DoPositionHandle(point, handleRotation);
    if (EditorGUI.EndChangeCheck()) {
        Undo.RecordObject(curve, "Move Point");
        EditorUtility.SetDirty(curve);
        curve.points[index] = handleTransform.InverseTransformPoint(point);
    }
    return point;
}
}

Preferably, I would like to be able to toggle the bezier curve symmetry on and off.

\$\endgroup\$
4
  • 1
    \$\begingroup\$ Let's say I move point A. Here are some policies for repositioning other points: 1. Keep the offset between A & CP1 fixed so CP1 moves exactly as A does, and CP2 moves to maintain symmetry 2. Scale & rotate the whole curve so it maintains its current shape but changes orientation/size. 3. Keep CP1 fixed, move CP2 to maintain symmetry (or vice versa) 4. Apply a fraction of A's movement to CP1, like a soft selection (depending on the softness factor this can range between options 1 & 3). 5. Calculate the displacement from symmetry and move each of CP1 and CP2 by half to correct it on each side… \$\endgroup\$
    – DMGregory
    Mar 9, 2018 at 12:53
  • \$\begingroup\$ @DMGregory option 1. is what I need. I believe the above options are all based on movement of A. In addition, if CP1 moves then CP2 moves in order to adjust the shape of the curve symmetry. \$\endgroup\$ Mar 9, 2018 at 14:26
  • \$\begingroup\$ Have you been able to make any progress? I was trying out something like this a while ago but didn't get far \$\endgroup\$ Mar 13, 2018 at 16:35
  • \$\begingroup\$ @OnlyCodeMatters No I haven't made progress \$\endgroup\$ Mar 14, 2018 at 8:55

1 Answer 1

4
\$\begingroup\$

Here's a method that seems to work OK.

First I created a little mirror structure to help make the symmetry math clearer:

public struct MirrorPlane {
    public readonly Vector3 normal;
    public readonly Vector3 center;

    public MirrorPlane(Vector3 direction, Vector3 center) {
        normal = direction.normalized;
        this.center = center;
    }

    public static MirrorPlane Between(Vector3 a, Vector3 b) {
        Vector3 difference = (b - a);
        Vector3 average = 0.5f * (a + b);

        return new MirrorPlane(difference, average);
    }

    public Vector3 Reflect(Vector3 point) {
        Vector3 difference = point - center;
        float signedDistance = Vector3.Dot(difference, normal);

        return point - 2f * signedDistance * normal;
    }
}

Then I added some symmetry properties to the BezierCurve MonoBehaviour:

public enum SymmetryMode {
    Off,               // Do not enforce symmetry.
    MirrorFarHandle,   // When moving A, CP2 mirrors CP1
    MirrorNearHandle   // When moving A, CP1 mirrors CP2
}


public SymmetryMode symmetry;

// How much should the non-mirrored control point follow
// when the endpoint is moved? 0 = stationary, 1 = match end motion.
[Range(0, 1)]
public float symmetryBlend;

Then I added a little structure to the editor script to keep track of changes inside the ShowPoint method:

struct Change {
    public int index;
    public Vector3 displacement;

    public static Change Unchanged {
        get {
            return new Change { index = -1 };
        }
    }

    public static implicit operator bool(Change change) {
        return change.index >= 0;
    } 
}

// ShowPoint now logs the change if it's making, if any.
private Vector3 ShowPoint(int index, ref Change change) {
    Vector3 point = handleTransform.TransformPoint(curve.points[index]);
    EditorGUI.BeginChangeCheck();
    point = Handles.DoPositionHandle(point, handleRotation);
    if (EditorGUI.EndChangeCheck()) {
        Undo.RecordObject(curve, "Move Point");
        EditorUtility.SetDirty(curve);

        // Log change.
        change.index = index;
        Vector3 newPoint = handleTransform.InverseTransformPoint(point);
        change.displacement = newPoint - curve.points[index];

        curve.points[index] = newPoint;
    }
    return point;
}

Lastly, the guts of the OnSceneGUI now have this:

// Initialize an empty change, and log changes made inside the ShowPoint()s
var change = Change.Unchanged;

Vector3 p0 = ShowPoint(0, ref change);
Vector3 p1 = ShowPoint(1, ref change);
Vector3 p2 = ShowPoint(2, ref change);
Vector3 p3 = ShowPoint(3, ref change);

// If there's a change we need to mirror symmetrically....
if (change && (curve.symmetry != BezierCurve.SymmetryMode.Off)) {
    // One control point will move to mirror the other.
    int lead;

    if (change.index == 1 || change.index == 2) {
        // If we moved one of the control points,
        // it leads, and the other follows to mirror it.
        lead = change.index;

    } else {
        // Otherwise, we moved an endpoint, 
        // and our symmetry mode determines which control point gets mirrored.
        bool mirrorCP2 = (change.index == 0)
                       ^ (curve.symmetry == BezierCurve.SymmetryMode.MirrorNearHandle);

        lead = (mirrorCP2) ? 1 : 2;

        // This lets us either move the non-mirrored control point
        // in step with the endpoint, or keep it stationary, or any intermediate.
        curve.points[lead] += curve.symmetryBlend * change.displacement;
    }
    // Select the other control point and mirror it from the leader.
    int follow = 3 - lead;
    var mirror = MirrorPlane.Between(curve.points[0], curve.points[3]);
    curve.points[follow] = mirror.Reflect(curve.points[lead]);
}
\$\endgroup\$
1

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .