Monday, June 4, 2018

How to update one Bezier curve as another is moved using a custom editor

Leave a Comment

I am creating Bézier curves using the code below which I got from here. I have also made a BezierPair game object which has two Bézier curves as child objects.

From the respective images below and BezierPair, where points[0]...points[3] is represented as P0...P3:

  1. I want P0 of each Bézier curve always remain the same when one is moved. In other words I want them to move together always, with the option of turning this movement off.

enter image description here

  1. Say P1 of both curves are apart. How can I make P1 of each curve move in the same direction covering the same distance?

enter image description here

  1. Say P2 of both curves are apart. How can I make P2 of one curve mirror P2 of another curve along a line joining P0 and P3? Note that the mirror line would be taken from the curve 1 in the example below because curve1's P2 is moved. If curve2's P2 is moved, then the mirror line will be taken from curve2's P0P3.

enter image description here

I don’t want to do this at run time. So a custom editor has to be used. I tried solving 1. in the code below but the position for the second curve wouldn’t update without my selecting BezierPair in the hierarchy window

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; } } 

BezierPair:

public class BezierPair : MonoBehaviour {   public GameObject bez1; public GameObject bez2;  public void setupCurves()   {     bez1 = GameObject.Find("Bez1");     bez2 = GameObject.Find("Bez2"); } } 

BezierPairEditor:

[CustomEditor(typeof(BezierPair))] public class BezierPairEditor : Editor {  private BezierPair bezPair;   public override void OnInspectorGUI() {     bezPair = target as BezierPair;      if (bezPair.bez1.GetComponent<BezierCurve>().points[0] != bezPair.bez2.GetComponent<BezierCurve>().points[0])     {         Vector3 assignPoint0 = bezPair.bez1.GetComponent<BezierCurve>().points[0];         bezPair.bez2.GetComponent<BezierCurve>().points[0] = assignPoint0;      }      if (GUI.changed)     {          EditorUtility.SetDirty(bezPair.bez1);         EditorUtility.SetDirty(bezPair.bez2);         Repaint();     } } 

2 Answers

Answers 1

I tried solving 1. in the code below but the position for the second curve wouldn’t update without my selecting BezierPair in the hierarchy window

This is because you are using OnInspectorGUI() callback inside your BezierPairEditor class. It is only executed when the inspector of BezierPairEditor is shown on your editor window. Thus if you do not click on the GameObject with the BezierPair component, the BezierPair's inspector is not shown, and the callback does not trigger.

Instead, use the EditorApplication.update delegate. Inside your BezierPairEditor class:

void OnEnable() {     EditorApplication.update += OnUpdate; //Register to the update callback }  void OnUpdate() {     //Implement your code to update positions here } 

Secondly, try caching your BezierCurve references. This is because you don't really want to run GetComponent inside every update loop, as it is costly. Again, inside your BezierPairEditor class (I will omit the code above for clarity, merge it yourself):

BezierCurve bez1C; BezierCurve bez2C;  void OnEnable() {     BezierPair pair = target as BezierPair;     bez1C = pair.bez1.GetComponent<BezierCurve>();     bez2C = pair.bez2.GetComponenet<BezierCurve>(); }  void OnUpdate() {     //Do something with bez1C and bez2C here } 

Finally, to solve points 1 to 3, since you did not specify, I shall assume that you are moving bez1 manually, and bez2 should follow. In the case where either one can be moved manually and the other should follow, I would recommend you to implement an isChanged check in your BezierCurve class. Then in your BezierPairEditor class, you can check which one has moved, and update the other one accordingly.

Inside BezierPairEditor class (again, I will omit the code above for clarity, merge it yourself):

Vector2 bez1CPrev1;  void OnEnable() {     bez1CPrev1 = bez1C.points[1];      //Ensure that they have the same starting point     //Either you shift just points[0] of bez2 to be the same as bez1, or you shift every point. Implement this yourself. }  void OnUpdate() {     Vector2 disp = bez1C.points[1] - bez1CPrev1;      //1.      bez2C.points[0] = bez1C.points[0];      //2.     bez2C.points[1] = bez2C.points[1] + disp;      //3. Here, we are gonna use Vector2.Reflect(). Why?      //Imagine throwing a ball from newP2(curve1) to P3(curve1).     //If the 'surface' that the ball hits has a normal of P0(curve1)-P3(curve1), you will have the ball bounce back to reach newP2(curve2).     //Effectively: newP2(curve2) = P3(curve1) + [newP2(curve2) - P3(curve1)]. The term in the square brackets, we will do it with Vector2.Reflect().     bez2C.points[2] = bez1C.points[3] + Vector2.Reflect(bez1C.points[3] - bez1C.points[2], (bez1C.points[0] - bez1C.points[3]).normalized);      //Draw your bezier curve etc. here      bez1CPrev1 = bez1C.points[1]; } 

Answers 2

EDITED:

I dont think you'll need your BezierPair class. I suggest you to add a reference to the other BezierCurve object you want to "pair" as a public field on the BezierCurve class (paired). That other curve will get "paired" with this curve. Once paired, the constraints on the movement may apply. You can control the desired behaviors with the 3 public bool fields behavior1, behavior2 and behavior3.

Note#1: Instead of calling the method DrawBezierCurve from the Editor, I added the [ExecuteInEditMode] to the Component Class. This way, You don't mix responsibilities between the Component and the Editor: The BezierCurve component draws itself on the scene, while the BezierCurveEditor only manages edit logic, like applying the constraints and drawing the handlers.

BezierCurve:

using UnityEngine;  [RequireComponent(typeof(LineRenderer))] [ExecuteInEditMode] // Makes Update() being called often even in Edit Mode public class BezierCurve : MonoBehaviour {    public Vector3[] points;   public int numPoints = 50;   // Curve that is paired with this curve   public BezierCurve paired;   public bool behavior1; // check on editor if you desired behavior 1 ON/OFF   public bool behavior2; // check on editor if you desired behavior 2 ON/OFF   public bool behavior3; // check on editor if you desired behavior 3 ON/OFF   LineRenderer lr;    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>();   }    void Update()   {     // This component is the only responsible for drawing itself.     DrawBezierCurve();   }    // This method is called whenever a field is changed on Editor   void OnValidate()   {     // This avoids pairing with itself     if (paired == this) paired = null;   }    void DrawBezierCurve()   {     lr.positionCount = numPoints;     for (int i = 0; i < numPoints; i++)     {       // This corrects the "strange" extra point you had with your script.       float t = i / (float)(numPoints - 1);       lr.SetPosition(i, GetPoint(t));     }   }    public Vector3 GetPoint(float t)   {     return transform.TransformPoint(Bezier.GetPoint(points[0], points[1], points[2], points[3], 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;   } } 

Note#2: The desired behavior were coded inside the handler drawing methods, so you have access to Undo and other features.

Note#3: EditorUtility.SetDirty is considered obsolete since Unity 5.3 for marking objects as dirty for drawing, and should no-longer be used for modifying objects in scenes. Undo.RecordObject does the job.

BezierCurveEditor:

using UnityEngine; using UnityEditor;  [CustomEditor(typeof(BezierCurve))] // This attribute allows you to select multiple curves and manipulate them all as a whole on Scene or Inspector [CanEditMultipleObjects] public class BezierCurveEditor : Editor {   BezierCurve curve;   Transform handleTransform;   Quaternion handleRotation;   const int lineSteps = 10;   const float directionScale = 0.5f;    BezierCurve prevPartner; // Useful later.    void OnSceneGUI()   {     curve = target as BezierCurve;     if (curve == null) return;     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);      // Handles multiple selection     var sel = Selection.GetFiltered(typeof(BezierCurve), SelectionMode.Editable);     if (sel.Length == 1)     {       // This snippet checks if you just attached or dettached another curve,       // so it updates the attached member in the other curve too automatically       if (prevPartner != curve.paired)       {         if (prevPartner != null) { prevPartner.paired = null; }         prevPartner = curve.paired;       }     }      if (curve.paired != null & curve.paired != curve)     {       // Pair the curves.       var partner = curve.paired;       partner.paired = curve;       partner.behavior1 = curve.behavior1;       partner.behavior2 = curve.behavior2;       partner.behavior3 = curve.behavior3;     }   }    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);     }   }    // Constraints for a curve attached to back   // The trick here is making the object being inspected the "master" and the attached object is adjusted to it.   // This way, you avoid the conflict of one object trying to move the other.   // [ExecuteInEditMode] on component class makes it posible to have real-time drawing while editing.   // If you were calling DrawBezierCurve from here, you would only see updates on the other curve when you select it   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" + index.ToString());       var thisPts = curve.points;       var pairPts = curve.paired.points;       switch (index)       {         case 0:           {             if (curve.behavior1)             {               pairPts[0] = curve.paired.transform.InverseTransformPoint(point);             }             break;           }         case 1:           {             if (curve.behavior2)             {               pairPts[1] += thisPts[1] - handleTransform.InverseTransformPoint(point);             }             break;           }         case 2:           {             if (curve.behavior1)             {               pairPts[2] = thisPts[3] + Vector3.Reflect(thisPts[3] - thisPts[2], (thisPts[0] - thisPts[3]).normalized);             }             break;           }         default:           break;       }       curve.points[index] = handleTransform.InverseTransformPoint(point);     }     return point;   } } 

For have it working, reference one BezierCurve to another's paired field through inspector, and set ON/OFF the behaviors you want.

Tips: tinker the properties of the LineRenderer to get cool gradients or width variation (like a brush stroke). If you have a cusp node and want it to look continuous, increase the value of End Cap Vertices on the Line Renderer. Use Sprites-Default as Material, for 2D.

If You Enjoyed This, Take 5 Seconds To Share It

0 comments:

Post a Comment