Search Unity

Creating bezier between 2 points on Canvas

Discussion in '2D Experimental Preview' started by MaskedMouse, Jun 24, 2019.

  1. MaskedMouse

    MaskedMouse

    Joined:
    Jul 8, 2014
    Posts:
    340
    Hello,

    I'm trying to create a runtime node graph and between 2 nodes I want to use a curved line.

    I try to use the Vector Graphics package, create a shape and use the FillMesh method. But I don't seem to get the mesh on screen.

    I've got a few points on my canvas I can drag around but the initial bezier doesn't seem to be valid.

    This is the script I used. The BezierPoints are initialized through the inspector.
    This is all just for testing on how to create a Bezier. Ofcourse the end result will probably be different like a static method just taking 2 points to generate a bezier between.
    But my goal is to create something like the Node graph of the Shader Graph but then runtime for the user.
    I just can't seem to figure out how. Anything I am doing wrong?
    The points on screen are:
    A = X -200, Y 0
    B = X 0, Y 0
    B1 = X 0, Y 50
    B2 = X 0, Y -50
    C = 200, Y 0

    Code (CSharp):
    1. using System;
    2. using System.Collections.Generic;
    3. using Unity.VectorGraphics;
    4. using UnityEngine;
    5.  
    6. [RequireComponent(typeof(MeshFilter))]
    7. public class BezierGenerator : MonoBehaviour
    8. {
    9.     public List<BezierPoint> BezierPoints;
    10.     private BezierPathSegment[] BezierPath;
    11.  
    12.     private Shape shape;
    13.     private Scene scene;
    14.     private VectorUtils.TessellationOptions tesselationOptions;
    15.  
    16.     private Mesh mesh;
    17.  
    18.     private void Start()
    19.     {
    20.         // Generate Shape, Scene and Options to tesselate
    21.         shape = new Shape
    22.         {
    23.             PathProps = new PathProperties { Stroke = new Stroke { Color = Color.red, HalfThickness = 1.0f } },
    24.             Contours = new[] { new BezierContour { Segments = new BezierPathSegment[BezierPoints.Count], Closed = false } },
    25.             Fill = new SolidFill()
    26.         };
    27.         scene = new Scene { Root = new SceneNode { Shapes = new List<Shape> { shape } } };
    28.         tesselationOptions = new VectorUtils.TessellationOptions { StepDistance = 500, MaxCordDeviation = 0.1f, MaxTanAngleDeviation = 0.1f, SamplingStepSize = 0.01f };
    29.  
    30.         // Generate Mesh
    31.         mesh = new Mesh();
    32.         GetComponent<MeshFilter>().mesh = mesh;
    33.    
    34.         // Generate the object on screen
    35.         Generate();
    36.     }
    37.  
    38.     public void Generate()
    39.     {
    40.         for (var index = 0; index < BezierPoints.Count; index++)
    41.         {
    42.             var point = BezierPoints[index];
    43.             var bezierSegment = shape.Contours[0].Segments[index];
    44.  
    45.             bezierSegment.P0 = point.MainPoint.localPosition;
    46.             if (point.PointA != null) bezierSegment.P1 = point.PointA.localPosition;
    47.             if (point.PointB != null) bezierSegment.P2 = point.PointB.localPosition;
    48.         }
    49.  
    50.         var geoms = VectorUtils.TessellateScene(scene, tesselationOptions);
    51.         VectorUtils.FillMesh(mesh, geoms, 1f);
    52.     }
    53.  
    54.     [Serializable]
    55.     public class BezierPoint
    56.     {
    57.         public Transform MainPoint;
    58.         public Transform PointA;
    59.         public Transform PointB;
    60.     }
    61. }
     
    Last edited: Jun 24, 2019
  2. MaskedMouse

    MaskedMouse

    Joined:
    Jul 8, 2014
    Posts:
    340
    I found out what was the problem...
    BezierPathSegment is a struct ofcourse I get a copy when changing the values. I have to re-apply them.
    I constructed this code from a demo script but maybe at that point it was a class not a struct. Thus the demo script is out of date.

    So changed the code to re-apply the BezierPathSegment.

    Now my next problem is, to create an actual nice bezier between 2 points... I'm no math boy
     
    Last edited: Jun 25, 2019
    mcoted3d likes this.
  3. mcoted3d

    mcoted3d

    Unity Technologies

    Joined:
    Feb 3, 2016
    Posts:
    309
    Here's what I would try (and this is what I think GraphView is doing). When connecting an output port (going to the right) to an input port (coming from the left), put the first control point at a fixed distance to the right of the output port, and the second control point to the left of the input port. The distance of the control ports can be diminished if the ports are near to each other, to avoid a very jiggly curve.
     
  4. MaskedMouse

    MaskedMouse

    Joined:
    Jul 8, 2014
    Posts:
    340
    Well I only have 2 main points now, Output and Input (P0). used P1 and P2 of the first P0 to get the nice curve.

    But the next problem is masking. Due to the bezier being a mesh we cannot mask it in our UI.
    But when creating a Sprite using the VectorUtility, the curve is not correct anymore...

    Heres where the Mesh doesn't get cut off by the Mask because it isn't a UI element.

    2019-06-25 16_30_41-.png

    And this is what happens when we replace
    VectorUtils.FillMesh
    with
    VectorUtils.BuildSprite

    image.sprite = VectorUtils.BuildSprite(geoms, 1, VectorUtils.Alignment.SVGOrigin, new Vector2(0.5f, 0.5f), 0);


    2019-06-25 16_35_33-.png


    Updated Script (Mesh):

    Code (CSharp):
    1. using System.Collections.Generic;
    2. using Unity.VectorGraphics;
    3. using UnityEngine;
    4. using UnityEngine.Profiling;
    5.  
    6. [RequireComponent(typeof(MeshFilter))]
    7. public class BezierGenerator : MonoBehaviour
    8. {
    9.     public Transform StartPoint;
    10.     public Transform StartPointTangent;
    11.  
    12.     public Transform EndPoint;
    13.     public Transform EndPointTangent;
    14.  
    15.     private Shape shape;
    16.     private Scene scene;
    17.     private VectorUtils.TessellationOptions tesselationOptions;
    18.  
    19.     private Mesh mesh;
    20.  
    21.     private void Awake()
    22.     {
    23.         // Generate Mesh
    24.         mesh = new Mesh();
    25.         GetComponent<MeshFilter>().mesh = mesh;
    26.     }
    27.  
    28.     private void Start()
    29.     {
    30.         // Generate Shape, Scene and Options to tesselate
    31.         shape = new Shape
    32.         {
    33.             PathProps = new PathProperties { Stroke = new Stroke { Color = Color.red, HalfThickness = 1.0f } },
    34.             Contours = new[] { new BezierContour { Segments = new BezierPathSegment[2], Closed = false } },
    35.             Fill = new SolidFill()
    36.         };
    37.         scene = new Scene { Root = new SceneNode { Shapes = new List<Shape> { shape } } };
    38.         tesselationOptions = new VectorUtils.TessellationOptions { StepDistance = 500, MaxCordDeviation = 0.1f, MaxTanAngleDeviation = 0.1f, SamplingStepSize = 0.01f };
    39.  
    40.         // Generate the object on screen
    41.         Generate();
    42.     }
    43.  
    44.     public void Generate()
    45.     {
    46.         Profiler.BeginSample("Generate Bezier");
    47.         var contour = shape.Contours[0];
    48.         var startSegment = contour.Segments[0];
    49.         var endSegment = contour.Segments[1];
    50.  
    51.         startSegment.P0 = StartPoint.localPosition;
    52.         StartPointTangent.localPosition = Vector2.Distance(StartPoint.localPosition, EndPoint.localPosition) * 0.55f * Vector2.right;
    53.         EndPointTangent.localPosition = -StartPointTangent.localPosition;
    54.         startSegment.P1 = transform.InverseTransformPoint(StartPointTangent.position);
    55.         startSegment.P2 = transform.InverseTransformPoint(EndPointTangent.position);
    56.  
    57.         endSegment.P0 = EndPoint.localPosition;
    58.         contour.Segments[0] = startSegment;
    59.         contour.Segments[1] = endSegment;
    60.  
    61.         var geoms = VectorUtils.TessellateScene(scene, tesselationOptions);
    62.         VectorUtils.FillMesh(mesh, geoms, 1f);
    63.         Profiler.EndSample();
    64.     }
    65. }
    66.  
     
    Last edited: Jun 25, 2019
  5. mcoted3d

    mcoted3d

    Unity Technologies

    Joined:
    Feb 3, 2016
    Posts:
    309
    There shouldn't be any difference when building a sprite object, apart from the alignment property. If you can show me the sprite building code I could probably help.
     
  6. MaskedMouse

    MaskedMouse

    Joined:
    Jul 8, 2014
    Posts:
    340
    image.sprite = VectorUtils.BuildSprite(geoms, 1, VectorUtils.Alignment.SVGOrigin, new Vector2(0.5f, 0.5f), 0);


    Thats all there is to it.
    Just a regular Image component.
    What I did notice was... when the beziercontour “closed” property is set to false it still creates a closed mesh, but renders it without. But when the sprite is built it seems to ignore the closed property.
    It still closes the mesh and then renders it to a sprite it seems.

    It’s the end of my working day, will continue this tomorrow. I could share the unity project.
     
  7. mcoted3d

    mcoted3d

    Unity Technologies

    Joined:
    Feb 3, 2016
    Posts:
    309
    That's probably the issue, you'll have to use an SVGImage component.
     
  8. MaskedMouse

    MaskedMouse

    Joined:
    Jul 8, 2014
    Posts:
    340
    Yeah that seemed to be the problem, I've used the SVGImage component now and it does Render the bezier fine within the RectTransform.

    Now my next problem is.. alligning it between those 2 points.. another challenge
    As it renders it fully within its rectangle it is now misalligned.

    2019-06-26 08_45_59-Window.png

    I have tried to set the RectTransform using some calculations.
    The SVG image is now a child of the BezierGenerator. (same level as the points)

    Code (CSharp):
    1. var newRect = VectorUtils.ApproximateSceneNodeBounds(scene.Root);
    2.         var newPosition = VectorUtils.Eval(new BezierSegment {P0 = startSegment.P0, P1 = startSegment.P1, P2 = startSegment.P2, P3 = endSegment.P0}, 0.5f);
    3.         imageRect.localPosition = newPosition;
    4.         imageRect.sizeDelta = newRect.size;
    But this doesn't work because the actual mesh generated is a closed mesh. Even though Closed is set to false. I need the bounds of the non closed mesh but can only get the bounds of the closed mesh. So I will probably have to calculate the bounds myself.

    @mcoted3d Why does it still generate a closed mesh when specifically told not to?

    Closed Mesh.png
     
    Last edited: Jun 26, 2019
  9. mcoted3d

    mcoted3d

    Unity Technologies

    Joined:
    Feb 3, 2016
    Posts:
    309
    You may have to carefully choose the alignment when building the sprite (a bottom-left alignment should give you the equivalent of a normal mesh, unless I'm missing something).

    Hard to tell, make sure that you aren't sending a copy of the contour which has
    Closed=true
    (
    BezierContour
    is a struct).
     
  10. MaskedMouse

    MaskedMouse

    Joined:
    Jul 8, 2014
    Posts:
    340
    @mcoted3d Setting the allignment of the sprite has 0 effect when I change it.
    The mesh generated is not bound to the RectTransform.
    The sprite generated is bound to the RectTransform, hence that in code I have to calculate the size of the RectTransform and then set the sprite to that. If the RectTransform size is too wide it will stretch, if it is too small it will squash.

    Also the contour has never been set to true, on Start I create the contour with "Closed = false" which is then edited in the Generate method. That shouldn't change on its own right? But yet when filling the mesh or building the sprite you can see that it still created a closed mesh. It is not rendering it on screen, but still generated it. If this weren't the case the code I have would've already worked. As I pick the boundaries and set the rect size of the boundaries.

    Here's a link to the Unity project, using Unity 2018.4.2f1.
    https://maskedmous.stackstorage.com/s/g28OccMuArnYcKO

    Bezier 1 - 3 are the generators (they contain a mesh renderer, which the Bezier Generator fills the mesh on)
    their children have a "Generated Bezier" game object which are the SVG Image counter parts of the generated bezier.

    The SVG Image component on there is an extended version which contains a Preserve Aspect.
    In play mode you can drag around the white input / output squares.

    The magic of the Bezier Generation happens in BezierGenerator.cs with a few comments in the code.
    Generate method is changing the values and building the mesh & sprite.
    The mesh version is what I'm trying to achieve but then as a Sprite. You can toggle the mesh renderer on / off to see what the actual result should be.
     
    Last edited: Jun 27, 2019
  11. mcoted3d

    mcoted3d

    Unity Technologies

    Joined:
    Feb 3, 2016
    Posts:
    309
    The shape seems to be closed because of the
    SolidFill()
    that is set on the shape object. Leaving the fill to null should fix your issue.
     
  12. MaskedMouse

    MaskedMouse

    Joined:
    Jul 8, 2014
    Posts:
    340
    Ah, I've changed it to null and I've optimized it a little bit and now the sprite generates as it should!
    I've got the mesh bounds from the geoms[0] using the VectorUtils and applied that to my RectTransform.
    It's generating average 14 kb of garbage but that's okay-ish. It could be optimized though, I noticed the
    VectorUtils.TessellatePath
    method uses
    Code (CSharp):
    1. List<Vector2> verts = new List<Vector2>(approxStepCount * 2 + 32); // A little bit possibly for the endings
    2. List<UInt16> inds = new List<UInt16>((int)(verts.Capacity * 1.5f)); // Usually every 4 verts represent a quad that uses 6 indices
    You could optimize that by only allocating a list once with enough capacity to handle most beziers. (overloaded method maybe? so this is optional)
    That would limit the amount of garbage to a minimum right?

    I'm very happy with the result though! Thanks for your help @mcoted3d

    Updated Script:

    Code (CSharp):
    1. using System.Collections.Generic;
    2. using Unity.VectorGraphics;
    3. using UnityEngine;
    4. using UnityEngine.Profiling;
    5.  
    6. [RequireComponent(typeof(MeshFilter))]
    7. public class BezierGenerator : MonoBehaviour
    8. {
    9.     public Transform StartPoint;
    10.     public Transform StartPointTangent;
    11.  
    12.     public Transform EndPoint;
    13.     public Transform EndPointTangent;
    14.  
    15.     private Shape shape;
    16.     private Scene scene;
    17.     private VectorUtils.TessellationOptions tesselationOptions;
    18.  
    19.     private SVG_Extension.SVGImage image;
    20.     private RectTransform imageRect;
    21.  
    22.     private void Awake()
    23.     {
    24.         // Generate Mesh
    25.         image = GetComponentInChildren<SVG_Extension.SVGImage>();
    26.         imageRect = image.GetComponent<RectTransform>();
    27.     }
    28.  
    29.     private void Start()
    30.     {
    31.         // Generate Shape, Scene and Options to tesselate
    32.         shape = new Shape
    33.         {
    34.             PathProps = new PathProperties { Stroke = new Stroke { Color = Color.red, HalfThickness = 1.0f } },
    35.             Contours = new[] { new BezierContour { Segments = new BezierPathSegment[2], Closed = false } },
    36.             Fill = null
    37.         };
    38.         scene = new Scene { Root = new SceneNode { Shapes = new List<Shape> { shape } } };
    39.         tesselationOptions = new VectorUtils.TessellationOptions { StepDistance = 500, MaxCordDeviation = 0.1f, MaxTanAngleDeviation = 0.1f, SamplingStepSize = 0.01f };
    40.  
    41.         // Generate the object on screen
    42.         Generate();
    43.     }
    44.  
    45.     public void Generate()
    46.     {
    47.         Profiler.BeginSample("Generate Bezier");
    48.         // Get the segments from the contour
    49.         var contour = shape.Contours[0];
    50.         var startSegment = contour.Segments[0];
    51.         var endSegment = contour.Segments[1];
    52.  
    53.         // Change the segments with the new information
    54.         // Our starting & end point
    55.         startSegment.P0 = StartPoint.localPosition;
    56.         endSegment.P0 = EndPoint.localPosition;
    57.  
    58.         // Setting the position of the tangents
    59.         StartPointTangent.localPosition = Vector2.Distance(StartPoint.localPosition, EndPoint.localPosition) * 0.55f * Vector2.right;
    60.         EndPointTangent.localPosition = -StartPointTangent.localPosition;
    61.  
    62.         // Get the position of the tangent point relative to the parent of the start point. (which is the bezier generator itself)
    63.         startSegment.P1 = transform.InverseTransformPoint(StartPointTangent.position);
    64.         startSegment.P2 = transform.InverseTransformPoint(EndPointTangent.position);
    65.  
    66.         // Set the segments as they're structs
    67.         contour.Segments[0] = startSegment;
    68.         contour.Segments[1] = endSegment;
    69.         // Set contour
    70.         shape.Contours[0] = contour;
    71.  
    72.         // Create new geometry
    73.         var geoms = VectorUtils.TessellateScene(scene, tesselationOptions);
    74.         // Get the mesh bounds from the tessellated scene
    75.         var bounds = VectorUtils.Bounds(geoms[0].Vertices);
    76.         // Set the local position to the bounds center
    77.         imageRect.localPosition = bounds.center;
    78.         // Set the size of the rectangle to the bounds rectangle
    79.         imageRect.sizeDelta = bounds.size;
    80.  
    81.         // Build the sprite from the data
    82.         image.sprite = VectorUtils.BuildSprite(geoms, 1, VectorUtils.Alignment.Center, new Vector2(0.5f, 0.5f), 0);
    83.  
    84.         Profiler.EndSample();
    85.     }
    86. }
    87.  
     
    Last edited: Jun 27, 2019
  13. mcoted3d

    mcoted3d

    Unity Technologies

    Joined:
    Feb 3, 2016
    Posts:
    309
    Yes absolutely, recreating a new list seems wasteful there, we'll see what we can do!

    I'm happy to see that you got it working! Cheers!