SplineMesh, the plugin to create curved content

3322410--258630--bandeau github.png

3322410--391462--contortion.gif
This 2200 triangles beauty by Janpec updates at 800 fps on i5

v1.2: bend on any spline interval, not only on each curves. Better perfs.
v1.1: control the roll and scale at each node. Better API. Better perfs.

SplineMesh is a free and open-source plugin that allows you to create curved content :
- bend meshes,
- extrude 2D shapes,
- place assets along splines,
and anything you can imagine, thanks to a very simple and expandable tool set.

Get it on the Asset Store now !

Don't expect giant editors with tons of options here. SplineMesh encourages you to write your own scripts by providing concise documentation and a lot of simple exemples to expand.

Want to contribute? Consider giving feedback, credits, sending your code modifications or your own example via GitHub, and showing your creations here !

Finally, SplineMesh exists in paid version. No additional content here, buy this only to support the author :)

Hope you like it !

3322410--366544--scifi-facility medbay.JPG
3322410--366547--track.JPG




(thx to Thunderent's assets Temple Props)

3322410--258641--Showcase.PNG.jpg

9 Likes

As requested by many of you, i've just created a short video tutorial to create your first tentacle ^^

https://www.youtube.com/watch?v=-iQj0lYbqLE

3 Likes

I have no use for it now, but it looks sexy ;)

1 Like

After a month or so on the asset store, SplineMesh has received:
- 400+ downloads
- 5/5 stars from 11 reviewers
- 30$ of gross revenu with the paid version
- a lot of kind words by e-mail and twitter !

Thank you so much to all of you guys !

2 Likes

I'm having an issue with using multiple extruded meshes. It seems that only a single mesh can exist at once.

Hi @foyleman ,

Are you talking about extrusion with the extrusion component? in this case, only a single 2d shape is supported by the exemple included into the asset.

If you are talking about deforming multiple meshes on each curve of the spline, this can easily be done ! You will need to write your own component from the basic pipe example and add a list of meshes. Then for each curve, just give a different mesh from the list to the mesh deformer.

If you encounter an issue with this approach, or if you're trying a different approach, could you please provide some more information by private message ?

Yes, I am talking about extrusion with the extrusion component. I was actually able to solve it by generating a new mesh in Update like so:

    private void Update() {
        if (toUpdate) {
            if (mf.sharedMesh == null) {
                mf.sharedMesh = new Mesh();
            }
            GenerateMesh();
            toUpdate = false;
        }
    }

I also remove the "if null" in OnEnable, but I don't think that helped me.

THANK YOU for your reply!

1 Like

Hey, this asset seems really great so far, and would actually like to swap out my current spline system for it, but one issue I'm having is that I can't manually change the values of the nodes in the inspector. I've noticed a note in the code saying it should be possible? I'm on 2017.3, if that might be a factor


It's a known issue. Alas, I haven't time to fix it for the moment. Help is welcome !

I needed this too, so here's what I did.

I wanted to:

  • Show the splines drawn in the editor regardless of being selected so I could line multiple splines up. That's where void OnScene () comes into play.
  • The nodes were originally built for local coordinates. I wanted both local and world when editing the points.
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(Spline))]
public class SplineEditor : Editor {

    private const int QUAD_SIZE = 12;
    private Color CURVE_COLOR = new Color(0.8f, 0.8f, 0.8f);
    private Color CURVE_BUTTON_COLOR = new Color(0.8f, 0.8f, 0.8f);
    private Color DIRECTION_COLOR = Color.red;
    private Color DIRECTION_BUTTON_COLOR = Color.red;

    private enum SelectionType {
        Node,
        Direction,
        InverseDirection
    }

    private SplineNode selection;
    private SelectionType selectionType;
    private bool mustCreateNewNode = false;
    private SerializedProperty nodes;
    private Spline spline;

    private GUIStyle nodeButtonStyle, directionButtonStyle;

    SplineEditor ()
    {
        SceneView.onSceneGUIDelegate += OnScene;
    }

    private void OnEnable() {
        spline = (Spline)target;
        nodes = serializedObject.FindProperty("nodes");

        Texture2D t = new Texture2D(1, 1);
        t.SetPixel(0, 0, CURVE_BUTTON_COLOR);
        t.Apply();
        nodeButtonStyle = new GUIStyle();
        nodeButtonStyle.normal.background = t;

        t = new Texture2D(1, 1);
        t.SetPixel(0, 0, DIRECTION_BUTTON_COLOR);
        t.Apply();
        directionButtonStyle = new GUIStyle();
        directionButtonStyle.normal.background = t;
    }

    SplineNode AddClonedNode(SplineNode node) {
        int index = spline.nodes.IndexOf(node);
        SplineNode res = new SplineNode(node.position, node.direction);
        if (index == spline.nodes.Count - 1) {
            spline.AddNode(res);
        } else {
            spline.InsertNode(index + 1, res);
        }
        return res;
    }

    void DeleteNode(SplineNode node)
    {
        if (spline.nodes.Count > 2)
            spline.RemoveNode(node);
    }

    void OnScene(SceneView scene)
    {
        if (spline == null)
            return;

        // draw a bezier curve for each curve in the spline
        foreach (CubicBezierCurve curve in spline.GetCurves()) {
            Handles.DrawBezier(spline.transform.TransformPoint(curve.n1.position),
                spline.transform.TransformPoint(curve.n2.position),
                spline.transform.TransformPoint(curve.n1.direction),
                spline.transform.TransformPoint(curve.GetInverseDirection()),
                CURVE_COLOR,
                null,
                3);
        }
    }

    void OnSceneGUI()
    {
        Event e = Event.current;
        if (e.type == EventType.mouseDown)
        {
            Undo.RegisterCompleteObjectUndo(spline, "change spline topography");
            // if alt key pressed, we will have to create a new node if node position is changed
            if (e.alt) {
                mustCreateNewNode = true;
            }
        }
        if (e.type == EventType.mouseUp)
        {
            mustCreateNewNode = false;
        }

        // disable game object transform gyzmo
        if (Selection.activeGameObject == spline.gameObject) {
            Tools.current = Tool.None;
            if (selection == null && spline.nodes.Count > 0)
                selection = spline.nodes[0];
        }

        // draw a bezier curve for each curve in the spline
        foreach (CubicBezierCurve curve in spline.GetCurves()) {
            Handles.DrawBezier(spline.transform.TransformPoint(curve.n1.position),
                spline.transform.TransformPoint(curve.n2.position),
                spline.transform.TransformPoint(curve.n1.direction),
                spline.transform.TransformPoint(curve.GetInverseDirection()),
                CURVE_COLOR,
                null,
                3);
        }

        // draw the selection handles
        switch (selectionType) {
            case SelectionType.Node:
                // place a handle on the node and manage position change
                Vector3 newPosition = spline.transform.InverseTransformPoint(Handles.PositionHandle(spline.transform.TransformPoint(selection.position), Quaternion.identity));
                if (newPosition != selection.position) {
                    // position handle has been moved
                    if (mustCreateNewNode) {
                        mustCreateNewNode = false;
                        selection = AddClonedNode(selection);
                        selection.SetDirection(selection.direction + newPosition - selection.position);
                        selection.SetPosition(newPosition);
                    } else {
                        selection.SetDirection(selection.direction + newPosition - selection.position);
                        selection.SetPosition(newPosition);
                    }
                }
                break;
            case SelectionType.Direction:
                selection.SetDirection(spline.transform.InverseTransformPoint(Handles.PositionHandle(spline.transform.TransformPoint(selection.direction), Quaternion.identity)));
                break;
            case SelectionType.InverseDirection:
                selection.SetDirection(2 * selection.position - spline.transform.InverseTransformPoint(Handles.PositionHandle(2 * spline.transform.TransformPoint(selection.position) - spline.transform.TransformPoint(selection.direction), Quaternion.identity)));
                break;
        }

        // draw the handles of all nodes, and manage selection motion
        Handles.BeginGUI();
        foreach (SplineNode n in spline.nodes)
        {
            Vector3 guiPos = HandleUtility.WorldToGUIPoint(spline.transform.TransformPoint(n.position));
            if (n == selection) {
                Vector3 guiDir = HandleUtility.WorldToGUIPoint(spline.transform.TransformPoint(n.direction));
                Vector3 guiInvDir = HandleUtility.WorldToGUIPoint(spline.transform.TransformPoint(2 * n.position - n.direction));

                // for the selected node, we also draw a line and place two buttons for directions
                Handles.color = Color.red;
                Handles.DrawLine(guiDir, guiInvDir);

                // draw quads direction and inverse direction if they are not selected
                if (selectionType != SelectionType.Node) {
                    if (Button(guiPos, directionButtonStyle)) {
                        selectionType = SelectionType.Node;
                    }
                }
                if (selectionType != SelectionType.Direction) {
                    if (Button(guiDir, directionButtonStyle)) {
                        selectionType = SelectionType.Direction;
                    }
                }
                if (selectionType != SelectionType.InverseDirection) {
                    if (Button(guiInvDir, directionButtonStyle)) {
                        selectionType = SelectionType.InverseDirection;
                    }
                }
            } else {
                if (Button(guiPos, nodeButtonStyle)) {
                    selection = n;
                    selectionType = SelectionType.Node;
                }
            }
        }
        Handles.EndGUI();

        if (GUI.changed)
            EditorUtility.SetDirty(target);
    }

    bool Button(Vector2 position, GUIStyle style) {
        return GUI.Button(new Rect(position - new Vector2(QUAD_SIZE / 2, QUAD_SIZE / 2), new Vector2(QUAD_SIZE, QUAD_SIZE)), GUIContent.none, style);
    }

    public override void OnInspectorGUI() {
        serializedObject.Update();
        // hint
        EditorGUILayout.HelpBox("Hold Alt and drag a node to create a new one.", MessageType.Info);

        // delete button
        if(selection == null || spline.nodes.Count <= 2) {
            GUI.enabled = false;
        }
        if (GUILayout.Button("Delete selected node")) {
            Undo.RegisterCompleteObjectUndo(spline, "delete spline node");
            DeleteNode(selection);
            selection = null;
        }
        GUI.enabled = true;

        // nodes
        // This special editor prevent the user to modify the list because nodes are listened.
        // But I can't understand why these guys are not editable in the inspector...
        EditorGUILayout.PropertyField(nodes);
        EditorGUI.indentLevel += 1;
        if (nodes.isExpanded) {
            for (int i = 0; i < nodes.arraySize; i++) {

                //I made it editable
                Vector3 localPointV3;
                EditorGUILayout.LabelField("Node " + i + ":");

                EditorGUI.BeginChangeCheck();
                localPointV3 = EditorGUILayout.Vector3Field("World Position:", spline.transform.TransformPoint (spline.nodes [i].position));
                if (EditorGUI.EndChangeCheck()) {
                    Undo.RecordObject(spline, "Move Point");
                    spline.nodes [i].SetPosition (spline.transform.InverseTransformPoint (localPointV3));
                    EditorUtility.SetDirty(spline);
                }
                EditorGUI.BeginChangeCheck();
                localPointV3 = EditorGUILayout.Vector3Field("World Direction:", spline.transform.TransformPoint (spline.nodes [i].direction));
                if (EditorGUI.EndChangeCheck()) {
                    Undo.RecordObject(spline, "Move Point");
                    spline.nodes [i].SetDirection (spline.transform.InverseTransformDirection (localPointV3));
                    EditorUtility.SetDirty(spline);
                }

                EditorGUI.BeginChangeCheck();
                localPointV3 = EditorGUILayout.Vector3Field("Local Position:", spline.nodes [i].position);
                if (EditorGUI.EndChangeCheck()) {
                    Undo.RecordObject(spline, "Move Point");
                    spline.nodes [i].SetPosition (localPointV3);
                    EditorUtility.SetDirty(spline);
                }
                EditorGUI.BeginChangeCheck();
                localPointV3 = EditorGUILayout.Vector3Field("Local Direction:", spline.nodes [i].direction);
                if (EditorGUI.EndChangeCheck()) {
                    Undo.RecordObject(spline, "Move Point");
                    spline.nodes [i].SetDirection (localPointV3);
                    EditorUtility.SetDirty(spline);
                }

//                EditorGUILayout.PropertyField(nodes.GetArrayElementAtIndex(i), new GUIContent("Node " + i), true);
            }
        }
        EditorGUI.indentLevel -= 1;

        serializedObject.ApplyModifiedProperties();
    }

    [MenuItem("GameObject/3D Object/Spline")]
    public static void CreateSpline() {
        new GameObject("Spline", typeof(Spline));
    }

    public Vector3 ToVector3 (string s)
    {
        string[] temp = s.Substring (1, s.Length-2).Split (',');
        return new Vector3 (float.Parse(temp[0]), float.Parse(temp[1]), float.Parse(temp[2]));
    }
}

Next up, I wanted to extrude a shape along a spline and I wanted to have multiple instances that were exclusive. Previously you could only have 1 extruded mesh and they were all the same mesh if you had more than 1 instance. I also made sure to apply this new mesh to the mesh colliders if they existed.

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// Special component to extrude shape along a spline.
///
/// Note : This component is not lightweight and should be used as-is mostly for prototyping. It allows to quickly create meshes by
/// drawing only the 2D shape to extrude along the spline. The result is not intended to be used in a production context and you will most likely
/// create eventualy the mesh you need in a modeling tool to save performances and have better control.
///
/// The special editor of this component allow you to draw a 2D shape with vertices, normals and U texture coordinate. The V coordinate is set
/// for the whole spline, by setting the number of times the texture must be repeated.
///
/// All faces of the resulting mesh are smoothed. If you want to obtain an edge without smoothing, you will have to overlap two vertices and set two normals.
///
/// You can expand the vertices list in the inspector to access data and enter precise values.
///
/// This component doesn't offer much control as Unity is not a modeling tool. That said, you should be able to create your own version easily.
/// </summary>
[ExecuteInEditMode]
[RequireComponent(typeof(MeshFilter))]
[RequireComponent(typeof(MeshRenderer))]
[RequireComponent(typeof(Spline))]
public class SplineExtrusion : MonoBehaviour {

    private MeshFilter mf;

    public Spline spline;
    public float TextureScale = 1;
    public List<Vertex> ShapeVertices = new List<Vertex>();

    private bool toUpdate = true;

    /// <summary>
    /// Clear shape vertices, then create three vertices with three normals for the extrusion to be visible
    /// </summary>
    private void Reset() {
        ShapeVertices.Clear();
        ShapeVertices.Add(new Vertex(new Vector2(0, 0.5f), new Vector2(0, 1), 0));
        ShapeVertices.Add(new Vertex(new Vector2(1, -0.5f), new Vector2(1, -1), 0.33f));
        ShapeVertices.Add(new Vertex(new Vector2(-1, -0.5f), new Vector2(-1, -1), 0.66f));
        toUpdate = true;
        OnEnable();
    }

    private void OnValidate() {
        toUpdate = true;
    }

    private void OnEnable() {
        mf = GetComponent<MeshFilter>();
        spline = GetComponent<Spline>();
//        if (mf.sharedMesh == null) {
            mf.sharedMesh = new Mesh();
//        }
        spline.NodeCountChanged.AddListener(() => toUpdate = true);
        spline.CurveChanged.AddListener(() => toUpdate = true);
    }

    private void Update() {
        if (toUpdate) {
            if (mf.sharedMesh == null) {
                mf.sharedMesh = new Mesh();
            }
            GenerateMesh();
            toUpdate = false;
        }
    }

    private List<OrientedPoint> GetPath()
    {
        var path = new List<OrientedPoint>();
        for (float t = 0; t < spline.nodes.Count-1; t += 1/10.0f)
        {
            var point = spline.GetLocationAlongSpline(t);
            var rotation = CubicBezierCurve.GetRotationFromTangent(spline.GetTangentAlongSpline(t));
            path.Add(new OrientedPoint(point, rotation));
        }
        return path;
    }

    public void GenerateMesh() {
        List<OrientedPoint> path = GetPath();

        int vertsInShape = ShapeVertices.Count;
        int segments = path.Count - 1;
        int edgeLoops = path.Count;
        int vertCount = vertsInShape * edgeLoops;

        var triangleIndices = new List<int>(vertsInShape * 2 * segments * 3);
        var vertices = new Vector3[vertCount];
        var normals = new Vector3[vertCount];
        var uvs = new Vector2[vertCount];

        int index = 0;
        foreach(OrientedPoint op in path) {
            foreach(Vertex v in ShapeVertices) {
                vertices[index] = op.LocalToWorld(v.point);
                normals[index] = op.LocalToWorldDirection(v.normal);
                uvs[index] = new Vector2(v.uCoord, path.IndexOf(op) / ((float)edgeLoops)* TextureScale);
                index++;
            }
        }
        index = 0;
        for (int i = 0; i < segments; i++) {
            for (int j = 0; j < ShapeVertices.Count; j++) {
                int offset = j == ShapeVertices.Count - 1 ? -(ShapeVertices.Count - 1) : 1;
                int a = index + ShapeVertices.Count;
                int b = index;
                int c = index + offset;
                int d = index + offset + ShapeVertices.Count;
                triangleIndices.Add(c);
                triangleIndices.Add(b);
                triangleIndices.Add(a);
                triangleIndices.Add(a);
                triangleIndices.Add(d);
                triangleIndices.Add(c);
                index++;
            }
        }

        mf.sharedMesh.Clear();
        mf.sharedMesh.vertices = vertices;
        mf.sharedMesh.normals = normals;
        mf.sharedMesh.uv = uvs;
        mf.sharedMesh.triangles = triangleIndices.ToArray();
        MeshCollider findMC = GetComponent<MeshCollider> ();
        if (findMC != null)
            findMC.sharedMesh = mf.sharedMesh;
    }

    [Serializable]
    public class Vertex
    {
        public Vector2 point;
        public Vector2 normal;
        public float uCoord;

        public Vertex(Vector2 point, Vector2 normal, float uCoord)
        {
            this.point = point;
            this.normal = normal;
            this.uCoord = uCoord;
        }
    }

    public struct OrientedPoint
    {
        public Vector3 position;
        public Quaternion rotation;

        public OrientedPoint(Vector3 position, Quaternion rotation)
        {
            this.position = position;
            this.rotation = rotation;
        }

        public Vector3 LocalToWorld(Vector3 point)
        {
            return position + rotation * point;
        }

        public Vector3 LocalToWorldDirection(Vector3 dir)
        {
            return rotation * dir;
        }
    }
}
2 Likes

Wow these are very useful features, thanks for sharing :). Let the integration begin! If I come up with anything that might be useful to someone else, I'll post it :)

methusalah999 great job on the gizmos for the extrustion tool btw, works very nicely

1 Like

Thanks for the sharing !

Don't you mind if I use some of your code for the next SplineMesh release?

Could you show some visuals as a result here?

Thank you for the kind words !

SplineMesh is open-source and it is always a great pleasure to integrate contributions.

Hi @methusalah999

About following object with difference curve length issue, as i saw in your example :

rate += Time.deltaTime / DurationInSecond;

I tried to change DurationInSecond base on length ratio with the first curve, something like this :

float _baseLength = _curve[0].Length;

float _ratio = _curve[1]  / _baseLength;

rate += Time.deltaTime / (DurationInSecond / _ratio );

But it still not work, and i don't really understand about your ideas :

"You just have to make your object velocity dependent on a fixed speed".

In the "follow spline" exemple, the speed of the object is dependent on the spline length and the user-specified travelling duration.

If I understand correctly, what you want to do is to specify a speed for the follower, in world units per seconds.

I wrote a pseudo code for you. You will se the usage of Spline.GetTangentAlongSplineAtDistance, which is what you need here. I hope it helps.

    public float constantSpeed;
    private float traveledDistance = 0;

    void Update() {
        // we increase the traveled distance according to speed of the follower
        // and the elapsed time since last frame
        traveledDistance += constantSpeed * Time.deltaTime;

        // (optionl) if the traveled distance is longer than spline length,
        // we come back to the spline start to make the follower loop infinitly
        if(traveledDistance > spline.Length) {
            traveledDistance -= spline.Length;
        }

        // we update the follower position and rotation accordingly to the traveled distance
        follower.position = spline.GetLocationAlongSplineAtDistance(traveledDistance);
        follower.rotation = CubicBezierCurve.GetRotationFromTangent(spline.GetTangentAlongSplineAtDistance(traveledDistance));
    }

Thanks for your reply, i already tried this, it worked "but" it's not steady at all, object keep shaking when moving along the path. You can see it on my video.

This one using GetLocationAlongSplineAtDistance

https://vimeo.com/272018121

This one using GetLocationAlongSpline

https://vimeo.com/272017992

Do you know why this happen ? I think these 2 functions should make the same result ?

This is another problem.

Using rate, the calculation is very precise and I can't explain the shaking. Maybe the texture?

Using distance there is an interpolation to find the correcte sample. Can you go in CubicBezierCurve class, line 17 and change the number of samples to 300 or 1000? This will have a massive performance impact but you should discover that it becomes as precise as the search by rate.

private const int STEP_COUNT = 1000;

Could you paste your code here for me to check things?

After change to 300-500, it's perfect now, the "shaking" reduced a a lot.

Here is my code :

public class PlayerRoad : MonoBehaviour
{
    private Spline _spline;

    // Use this for initialization
    void Start ()
    {
        _spline = GetComponent<Spline> ();   
    }

    public Vector3 GetNextPosition (float rate)
    {
        return _spline.GetLocationAlongSplineAtDistance (rate);
    }

    public Quaternion GetNextRotation (float rate)
    {
        return CubicBezierCurve.GetRotationFromTangent (_spline.GetTangentAlongSplineAtDistance (rate));
    }
}

Game controller class

void Update()
    {
        rate += Time.deltaTime * _moveSpeed;
        _player.UpdatePosition (_playerRoad.GetNextPosition (rate), _playerRoad.GetNextRotation (rate));
    }

Player class

// Update is called once per frame
    public void UpdatePosition (Vector3 pos, Quaternion rot)
    {
        _myTransform.position = pos;
        _myTransform.rotation = rot;
    }

So it's a precision problem of the sample points. Maybe I should let the user choose the number of sample in the Spline component.

The best solution is to implement the Casteljau's divide-and-conquer algorithm, which allows to compute less samples where the curve is flat, and more where it is curvy. This way you can have an excellent precision while saving perfs.

Contribution appreciated :-)

hello, I'm wondering if you can help me debug this error: "Time must be between 0 and last node index (2). Given time was 5.755882."

Do you know what could be causing the time to exceed the node index?

Thanks very much


You are trying to use Spline.GetTangentAlongSpline with a time that exceeds the total spline time. The spline time is equal to the number of individual curve in the spline. Giving 5.75 means that you want to get a position at time 0.75 on the 6th curve, while your spline has only 2 curves.

I can't tell what leaded you to do that without seeing the code. Can you explain or paste some code?