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
using System;
using System.Collections.Generic;
using Unity.VectorGraphics;
using UnityEngine;
[RequireComponent(typeof(MeshFilter))]
public class BezierGenerator : MonoBehaviour
{
public List<BezierPoint> BezierPoints;
private BezierPathSegment[] BezierPath;
private Shape shape;
private Scene scene;
private VectorUtils.TessellationOptions tesselationOptions;
private Mesh mesh;
private void Start()
{
// Generate Shape, Scene and Options to tesselate
shape = new Shape
{
PathProps = new PathProperties { Stroke = new Stroke { Color = Color.red, HalfThickness = 1.0f } },
Contours = new[] { new BezierContour { Segments = new BezierPathSegment[BezierPoints.Count], Closed = false } },
Fill = new SolidFill()
};
scene = new Scene { Root = new SceneNode { Shapes = new List<Shape> { shape } } };
tesselationOptions = new VectorUtils.TessellationOptions { StepDistance = 500, MaxCordDeviation = 0.1f, MaxTanAngleDeviation = 0.1f, SamplingStepSize = 0.01f };
// Generate Mesh
mesh = new Mesh();
GetComponent<MeshFilter>().mesh = mesh;
// Generate the object on screen
Generate();
}
public void Generate()
{
for (var index = 0; index < BezierPoints.Count; index++)
{
var point = BezierPoints[index];
var bezierSegment = shape.Contours[0].Segments[index];
bezierSegment.P0 = point.MainPoint.localPosition;
if (point.PointA != null) bezierSegment.P1 = point.PointA.localPosition;
if (point.PointB != null) bezierSegment.P2 = point.PointB.localPosition;
}
var geoms = VectorUtils.TessellateScene(scene, tesselationOptions);
VectorUtils.FillMesh(mesh, geoms, 1f);
}
[Serializable]
public class BezierPoint
{
public Transform MainPoint;
public Transform PointA;
public Transform PointB;
}
}
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
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.
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.
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);
Updated Script (Mesh):
using System.Collections.Generic;
using Unity.VectorGraphics;
using UnityEngine;
using UnityEngine.Profiling;
[RequireComponent(typeof(MeshFilter))]
public class BezierGenerator : MonoBehaviour
{
public Transform StartPoint;
public Transform StartPointTangent;
public Transform EndPoint;
public Transform EndPointTangent;
private Shape shape;
private Scene scene;
private VectorUtils.TessellationOptions tesselationOptions;
private Mesh mesh;
private void Awake()
{
// Generate Mesh
mesh = new Mesh();
GetComponent<MeshFilter>().mesh = mesh;
}
private void Start()
{
// Generate Shape, Scene and Options to tesselate
shape = new Shape
{
PathProps = new PathProperties { Stroke = new Stroke { Color = Color.red, HalfThickness = 1.0f } },
Contours = new[] { new BezierContour { Segments = new BezierPathSegment[2], Closed = false } },
Fill = new SolidFill()
};
scene = new Scene { Root = new SceneNode { Shapes = new List<Shape> { shape } } };
tesselationOptions = new VectorUtils.TessellationOptions { StepDistance = 500, MaxCordDeviation = 0.1f, MaxTanAngleDeviation = 0.1f, SamplingStepSize = 0.01f };
// Generate the object on screen
Generate();
}
public void Generate()
{
Profiler.BeginSample("Generate Bezier");
var contour = shape.Contours[0];
var startSegment = contour.Segments[0];
var endSegment = contour.Segments[1];
startSegment.P0 = StartPoint.localPosition;
StartPointTangent.localPosition = Vector2.Distance(StartPoint.localPosition, EndPoint.localPosition) * 0.55f * Vector2.right;
EndPointTangent.localPosition = -StartPointTangent.localPosition;
startSegment.P1 = transform.InverseTransformPoint(StartPointTangent.position);
startSegment.P2 = transform.InverseTransformPoint(EndPointTangent.position);
endSegment.P0 = EndPoint.localPosition;
contour.Segments[0] = startSegment;
contour.Segments[1] = endSegment;
var geoms = VectorUtils.TessellateScene(scene, tesselationOptions);
VectorUtils.FillMesh(mesh, geoms, 1f);
Profiler.EndSample();
}
}
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.
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.
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?
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).
@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.
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.
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
List<Vector2> verts = new List<Vector2>(approxStepCount * 2 + 32); // A little bit possibly for the endings
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:
using System.Collections.Generic;
using Unity.VectorGraphics;
using UnityEngine;
using UnityEngine.Profiling;
[RequireComponent(typeof(MeshFilter))]
public class BezierGenerator : MonoBehaviour
{
public Transform StartPoint;
public Transform StartPointTangent;
public Transform EndPoint;
public Transform EndPointTangent;
private Shape shape;
private Scene scene;
private VectorUtils.TessellationOptions tesselationOptions;
private SVG_Extension.SVGImage image;
private RectTransform imageRect;
private void Awake()
{
// Generate Mesh
image = GetComponentInChildren<SVG_Extension.SVGImage>();
imageRect = image.GetComponent<RectTransform>();
}
private void Start()
{
// Generate Shape, Scene and Options to tesselate
shape = new Shape
{
PathProps = new PathProperties { Stroke = new Stroke { Color = Color.red, HalfThickness = 1.0f } },
Contours = new[] { new BezierContour { Segments = new BezierPathSegment[2], Closed = false } },
Fill = null
};
scene = new Scene { Root = new SceneNode { Shapes = new List<Shape> { shape } } };
tesselationOptions = new VectorUtils.TessellationOptions { StepDistance = 500, MaxCordDeviation = 0.1f, MaxTanAngleDeviation = 0.1f, SamplingStepSize = 0.01f };
// Generate the object on screen
Generate();
}
public void Generate()
{
Profiler.BeginSample("Generate Bezier");
// Get the segments from the contour
var contour = shape.Contours[0];
var startSegment = contour.Segments[0];
var endSegment = contour.Segments[1];
// Change the segments with the new information
// Our starting & end point
startSegment.P0 = StartPoint.localPosition;
endSegment.P0 = EndPoint.localPosition;
// Setting the position of the tangents
StartPointTangent.localPosition = Vector2.Distance(StartPoint.localPosition, EndPoint.localPosition) * 0.55f * Vector2.right;
EndPointTangent.localPosition = -StartPointTangent.localPosition;
// Get the position of the tangent point relative to the parent of the start point. (which is the bezier generator itself)
startSegment.P1 = transform.InverseTransformPoint(StartPointTangent.position);
startSegment.P2 = transform.InverseTransformPoint(EndPointTangent.position);
// Set the segments as they're structs
contour.Segments[0] = startSegment;
contour.Segments[1] = endSegment;
// Set contour
shape.Contours[0] = contour;
// Create new geometry
var geoms = VectorUtils.TessellateScene(scene, tesselationOptions);
// Get the mesh bounds from the tessellated scene
var bounds = VectorUtils.Bounds(geoms[0].Vertices);
// Set the local position to the bounds center
imageRect.localPosition = bounds.center;
// Set the size of the rectangle to the bounds rectangle
imageRect.sizeDelta = bounds.size;
// Build the sprite from the data
image.sprite = VectorUtils.BuildSprite(geoms, 1, VectorUtils.Alignment.Center, new Vector2(0.5f, 0.5f), 0);
Profiler.EndSample();
}
}