Curved Roads

I’m building a road tool for a city building game. How can I make it allow curved roads to be built?

example

The tool im developing so far lets you construct a road from point a to point b by stretching a road segment. I’m thinking i’ll have to cut segments in the mesh somehow by script in order for it to bend.

Holy jeez Roger, that looks amazing. Do you mean, you cant bend the road any more than in the video? Not sure really. Cool stuff tho. I am very interested in seeing how you are doing this. Do you have any store assets playing a major roll in this?

lol, I apologize. Thats actually from a game. I’m trying to replicate that.

heres what mine is so far

I am not using any store assets for this.

I would say, that if your dragging vector, normalised, direction you are going, remains with in a certain range, that the road remains x piece. If you alter you direction vector, say up left, then, your road piece autmatically switches to road piece y. Road piece y is a curved piece.

Try here:

hmm, you mean have the road change to a curved road once its at a certain angle? That seems kind of hard to visualize. Because there is so many different angles and lengths the player may decide to go.

Do you know if theres a way to cut segments into a mesh? Sort of like modeling by code.

This is another example of how it can be done… And you can download the tool for free.

I’ve looked into that tool. Its a great tool, although I want to learn how to build my own. So I can modify it without confusion. I also want mine to be a tool players can use in game.

does anyone have a clue how this can be done?

Why not have a closer look at the code of the roads and path tool?

You build a cardinal spline with enough amount of nodes and construct a road mesh (a plane with enough vertices) to bend according to the spline.

Thats it.

You make it sound so easy hehe.

I cant even begin to understand whats going on.

how do I build a cardinal spline? And how do I construct a road mesh with enough vertices? I dont understand how you can transform geometry by script.

See? You just started asking right questions, now go read the documents and begin learning step by step.

Dont forget, google is always your best friend.

I have been looking around everywhere about this issue. I have not found any information about transforming geometry by script. Or whats the commands in order to do that. I have found many pages talking about splines in general and how they work. But there not geared toward Unity. Or maybe they are but their using an existing tool.

If you can post links to places that explain what I need to know in order to make a curved road tool, then that would help.

You can transform geometry by accessing the mesh data of the object. The mesh data is stored in a MeshFilter component. You can access it like this.

C#

//to access the data for the specific mesh for the object
Mesh objMeshData = GetComponent<MeshFilter>().mesh;

//to access mesh the data for all of the objects with the same mesh
Mesh sharedMeshData = GetComponent<MeshFilter>().sharedMesh;

JS

//to access the data for the specific mesh for the object
var objMeshData : Mesh = GetComponent.<MeshFilter>().mesh;

//to access mesh the data for all of the objects with the same mesh
var sharedMeshData : Mesh = GetComponent.<MeshFilter>().sharedMesh;

Note that .mesh gets the mesh for that specific object, whereas .sharedMesh gets the shared data for all of the objects with the same mesh. Therefore, use .mesh if you are editing only one object, and use .sharedMesh for editing many objects at once. It is not recommended however to modify .sharedMesh since you might modify imported assets and all objects that use that mesh will also be affected. Then change .vertices, .triangles, .uv, and .normals to affect your geometry.

Here is a catmullrom spline evaluation:

	//CatmullRom
	public static Vector3 CatmullRom(Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, float a) {
		float a2 = a * a;
		float a3 = a * a2;
		Vector3 result;
		//0.5f * ((((2.0f * v2) + ((-v1 + v3) * a)) + 
		//(((((2.0f * v1) - (5.0f * v2)) + (4.0f * v3)) - v4) * a2)) +
		//((((-v1 + (3.0f * v2)) - (3.0f * v3)) + v4) * a3));
		result.x = 0.5f *  ((((2.0f * v2.x) + ((-v1.x + v3.x) * a)) +
								(((((2.0f * v1.x) - (5.0f * v2.x)) + (4.0f * v3.x)) - v4.x) * a2)) +
								((((-v1.x + (3.0f * v2.x)) - (3.0f * v3.x)) + v4.x) * a3));
		result.y = 0.5f *  ((((2.0f * v2.y) + ((-v1.y + v3.y) * a)) +
								(((((2.0f * v1.y) - (5.0f * v2.y)) + (4.0f * v3.y)) - v4.y) * a2)) +
								((((-v1.y + (3.0f * v2.y)) - (3.0f * v3.y)) + v4.y) * a3));
		result.z = 0.5f *  ((((2.0f * v2.z) + ((-v1.z + v3.z) * a)) +
								(((((2.0f * v1.z) - (5.0f * v2.z)) + (4.0f * v3.z)) - v4.z) * a2)) +
								((((-v1.z + (3.0f * v2.z)) - (3.0f * v3.z)) + v4.z) * a3));
		return result;
	}

And here is how you visualise a closed catmullrom spline if you want to see: (nodes is an array of your vector3s, it must be minimum of 4 nodes) (q0, q1 are dummy vectors)

	void OnDrawGizmos () {
		for(int i = 0; i < nodes.Length; i++) {
			Gizmos.color = Color.red;
			Gizmos.DrawWireCube(nodes[i], new Vector3(1, 1, 1));
		}

		//To draw closed loop
		Gizmos.color = Color.yellow;
		for(int p = 0; p < nodes.Length; p++) {
			Vector3 p0 = nodes[(p-1+nodes.Length) % nodes.Length];
			Vector3 p1 = nodes[p];
			Vector3 p2 = nodes[(p+1+nodes.Length) % nodes.Length];
			Vector3 p3 = nodes[(p+2+nodes.Length) % nodes.Length];
			q0 = p1;
			for(int i = 1; i <= 10; i++) {
				float t = i / 10f;
				q1 = C_Vec3.CatmullRom(p0, p1, p2, p3, t);
				Gizmos.DrawLine(q0, q1);
				q0 = q1;
			}
		}
	}

Hope it helps.

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

public class Line
{
	public readonly IList<Vector3> points;
	public readonly Color color;
	public readonly float width;
	public readonly int segments;

	public Line(IList<Vector3> points, Color color, float width)
		: this(points, color, width, 1)
	{
	}

	public Line(IList<Vector3> points, Color color, float width, int segments)
	{
		#region Asserts
#if DEBUG
		if (points == null)
		{
			throw new ArgumentNullException("points");
		}
		if (points.Count < 2)
		{
			throw new ArgumentException("Line consists of less than 2 points");
		}
		if (width <= 0f)
		{
			throw new ArgumentException("Width must be positive");
		}
		if (segments <= 0)
		{
			throw new ArgumentException("Segments must be positive");
		}
#endif
		#endregion

		this.points = points;
		this.color = color;
		this.width = width;
		this.segments = segments;
	}
}
using System;
using System.Collections.Generic;
using UnityEngine;

public class Spline3D
{
	private readonly IList<Vector3> points;

	public Spline3D(IList<Vector3> controlPoints)
	{
		#region Asserts
#if DEBUG
		if (controlPoints == null)
		{
			throw new ArgumentNullException("controlPoints");
		}
		if (controlPoints.Count < 2)
		{
			throw new ArgumentException("Too few control points");
		}
#endif
		#endregion

		points = new List<Vector3>(controlPoints);
	}

	public int Length
	{
		get { return points.Count; }
	}

	public void RemoveAt(int index)
	{
		#region Asserts
#if DEBUG
		if (index < 0 || index >= points.Count)
		{
			throw new ArgumentOutOfRangeException("index");
		}
#endif
		#endregion

		points.RemoveAt(index);
	}

	public Vector3 this[int index]
	{
		get
		{
			#region Asserts
#if DEBUG
			if (index < 0 || index >= points.Count)
			{
				throw new ArgumentOutOfRangeException("index");
			}
#endif
			#endregion

			return points[index];
		}
		set
		{
			#region Asserts
#if DEBUG
			if (index < 0 || index >= points.Count)
			{
				throw new ArgumentOutOfRangeException("index");
			}
#endif
			#endregion

			points[index] = value;
		}
	}
	public Vector3 GetValue(float x)
	{
		if (x <= 0)
		{
			return points[0] - (0 - x) * GetDerivative(0);
		}
		if (x >= points.Count - 1)
		{
			return points[points.Count - 1] + (x - (points.Count - 1)) * GetDerivative(points.Count - 1);
		}

		var k = (int)x;
		var t = x - k;

		var d0 = GetDerivative(k);
		var d1 = GetDerivative(k + 1);
		var f0 = points[k];
		var f1 = points[k + 1];

		var a = 2 * (f0 - f1) + d0 + d1;
		var b = 3 * (f1 - f0) - 2 * d0 - d1;
		var c = d0;
		var d = f0;

		return a * t * t * t + b * t * t + c * t + d;
	}

	private Vector3 GetDerivative(int k)
	{
		if (k <= 0)
		{
			return points[1] - points[0];
		}

		if (k >= points.Count - 1)
		{
			return points[points.Count - 1] - points[points.Count - 2];
		}

		return (points[k + 1] - points[k - 1]) * .5f;
	}
}
using System;
using System.Collections.Generic;
using UnityEngine;

public class SplineRenderer : MonoBehaviour
{
	public Material material;

	private IDictionary<object, IList<Line>> data;
	private MeshFilter meshFilter;
	private MeshRenderer meshRenderer;
	private bool isChanged;

	public static SplineRenderer FindInstance()
	{
		return (SplineRenderer)FindObjectOfType(typeof(SplineRenderer));
	}

	private void Awake()
	{
		data = new Dictionary<object, IList<Line>>();
		meshFilter = gameObject.AddComponent<MeshFilter>();
		meshRenderer = gameObject.AddComponent<MeshRenderer>();
		meshRenderer.sharedMaterial = material;
	}

	private void Update()
	{
		if (isChanged)
		{
			isChanged = false;
			RegenerateMesh();
		}
	}

	public void AddLine(object key, Line line)
	{
		#region Asserts
#if DEBUG
		if (key == null)
		{
			throw new ArgumentNullException("key");
		}
		if (line == null)
		{
			throw new ArgumentNullException("line");
		}
#endif
		#endregion

		if (data.ContainsKey(key) == false)
		{
			data.Add(key, new List<Line>());
		}
		data[key].Add(line);

		isChanged = true;
	}

	public void RemoveLines(object key)
	{
		#region Asserts
#if DEBUG
		if (key == null)
		{
			throw new ArgumentNullException("key");
		}
#endif
		#endregion

		if (data.Remove(key))
		{
			isChanged = true;
		}
	}

	private IEnumerable<Line> GetLines()
	{
		foreach (var lines in data.Values)
		{
			foreach (var line in lines)
			{
				yield return line;
			}
		}
	}

	private void RegenerateMesh()
	{
		if (meshFilter.sharedMesh != null)
		{
			Destroy(meshFilter.sharedMesh);
		}

		var geometry = new Geometry();
		foreach (var line in GetLines())
		{
			AppendLine(geometry, line);
		}

		meshFilter.sharedMesh = geometry.ConvertToMesh();
	}

	private void AppendLine(Geometry geometry, Line line)
	{
		var spline = new Spline3D(line.points);

		var forward = (spline[1] - spline[0]).normalized;
		var left = Vector3.Cross(forward, Vector3.up);
		var point = spline.GetValue(0);

		geometry.vertices.Add(point + left * line.width / 2);
		geometry.vertices.Add(point - left * line.width / 2);
		geometry.normals.Add(Vector3.up);
		geometry.normals.Add(Vector3.up);
		geometry.uvs.Add(new Vector2(0, 0));
		geometry.uvs.Add(new Vector2(1, 0));
		geometry.colors.Add(line.color);
		geometry.colors.Add(line.color);

		var previousPoint = point;

		for (int i = 1; i < line.segments * spline.Length; i++)
		{
			var t = i / (float)line.segments;
			point = spline.GetValue(t);

			forward = (point - previousPoint).normalized;
			previousPoint = point;

			left = Vector3.Cross(forward, Vector3.up);

			var vertexCount = geometry.vertices.Count;

			geometry.vertices.Add(point + left * line.width / 2);
			geometry.vertices.Add(point - left * line.width / 2);
			geometry.normals.Add(Vector3.up);
			geometry.normals.Add(Vector3.up);
			geometry.uvs.Add(new Vector2(0, t));
			geometry.uvs.Add(new Vector2(1, t));
			geometry.colors.Add(line.color);
			geometry.colors.Add(line.color);

			geometry.triangles.Add(vertexCount + 0);
			geometry.triangles.Add(vertexCount + 1);
			geometry.triangles.Add(vertexCount - 2);
			geometry.triangles.Add(vertexCount + 1);
			geometry.triangles.Add(vertexCount - 1);
			geometry.triangles.Add(vertexCount - 2);
		}
	}
}
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

public class Geometry
{
	public readonly List<Vector3> vertices;
	public readonly List<Vector3> normals;
	public readonly List<Color> colors;
	public readonly List<Vector2> uvs;
	public readonly List<int> triangles;

	public Geometry()
	{
		vertices = new List<Vector3>();
		normals = new List<Vector3>();
		colors = new List<Color>();
		uvs = new List<Vector2>();
		triangles = new List<int>();
	}

	public Geometry(Mesh mesh)
	{
		#region Asserts
#if DEBUG
		if (mesh == null)
		{
			throw new ArgumentNullException("mesh");
		}
#endif
		#endregion

		vertices = new List<Vector3>(mesh.vertices);
		normals = new List<Vector3>(mesh.normals);
		colors = new List<Color>(mesh.colors);
		uvs = new List<Vector2>(mesh.uv);
		triangles = new List<int>(mesh.triangles);
	}

	public bool IsEmpty()
	{
		return vertices.Count == 0;
	}

	public void Clear()
	{
		vertices.Clear();
		normals.Clear();
		colors.Clear();
		uvs.Clear();
		triangles.Clear();
	}

	public static Mesh ConvertToSubmeshes(List<Geometry> geometries)
	{
		var vertices = new List<Vector3>();
		var uvs = new List<Vector2>();
		var normals = new List<Vector3>();
		var colors = new List<Color>();
		var tangents = new List<Vector4>();

		foreach (var geometry in geometries)
		{
			vertices.AddRange(geometry.vertices);
			uvs.AddRange(geometry.uvs);
			normals.AddRange(geometry.normals);
			colors.AddRange(geometry.colors);
			tangents.AddRange(geometry.CalculateTangents());
		}

		var mesh = new Mesh
			{
				vertices = vertices.ToArray(),
				uv = uvs.ToArray(),
				normals = normals.ToArray(),
				colors = colors.ToArray(),
				tangents = tangents.ToArray(),
				subMeshCount = geometries.Count,
			};

		int vertexShift = 0;
		for (int submeshIndex = 0; submeshIndex < geometries.Count; submeshIndex++)
		{
			var geometry = geometries[submeshIndex];

			var triangles = geometry.triangles.Select(t => t + vertexShift).ToArray();
			mesh.SetTriangles(triangles, submeshIndex);

			vertexShift += geometry.vertices.Count;
		}

		return mesh;
	}

	public Mesh ConvertToMesh()
	{
		if (triangles.Count % 3 != 0)
		{
			Debug.Log(string.Format("Vertices: {0}, Tris: {1}, Colors: {2}, Normals: {3}, Uvs: {4}", vertices.Count, triangles.Count, colors.Count, normals.Count, uvs.Count));
		}

		var mesh = new Mesh
			{
				vertices = vertices.ToArray(),
				triangles = triangles.ToArray(),
			};

		if (uvs != null)
		{
			mesh.normals = normals.ToArray();
			mesh.uv = uvs.ToArray();
			mesh.colors = colors.ToArray();
			if (uvs.Count == vertices.Count)
			{
				mesh.tangents = CalculateTangents();
			}
		}

		return mesh;
	}

	public int AppendGeometry(Vector3 position, Geometry geometry)
	{
		#region Asserts
#if DEBUG
		if (geometry == null)
		{
			throw new ArgumentNullException("geometry");
		}
#endif
		#endregion

		return AppendGeometry(position, geometry.vertices, geometry.normals, geometry.uvs, geometry.colors, geometry.triangles);
	}

	#region Private

	private Vector4[] CalculateTangents()
	{
		int triangleCount = triangles.Count;
		int vertexCount = vertices.Count;

		var tan1 = new Vector3[vertexCount];
		var tan2 = new Vector3[vertexCount];

		var tangents = new Vector4[vertexCount];

		for (int a = 0; a < triangleCount; a += 3)
		{
			int i1 = triangles[a + 0];
			int i2 = triangles[a + 1];
			int i3 = triangles[a + 2];

			var v1 = vertices[i1];
			var v2 = vertices[i2];
			var v3 = vertices[i3];

			var w1 = uvs[i1];
			var w2 = uvs[i2];
			var w3 = uvs[i3];

			float x1 = v2.x - v1.x;
			float x2 = v3.x - v1.x;
			float y1 = v2.y - v1.y;
			float y2 = v3.y - v1.y;
			float z1 = v2.z - v1.z;
			float z2 = v3.z - v1.z;

			float s1 = w2.x - w1.x;
			float s2 = w3.x - w1.x;
			float t1 = w2.y - w1.y;
			float t2 = w3.y - w1.y;

			float r = 1.0f / (s1 * t2 - s2 * t1);

			var sdir = new Vector3((t2 * x1 - t1 * x2) * r, (t2 * y1 - t1 * y2) * r, (t2 * z1 - t1 * z2) * r);
			var tdir = new Vector3((s1 * x2 - s2 * x1) * r, (s1 * y2 - s2 * y1) * r, (s1 * z2 - s2 * z1) * r);

			tan1[i1] += sdir;
			tan1[i2] += sdir;
			tan1[i3] += sdir;

			tan2[i1] += tdir;
			tan2[i2] += tdir;
			tan2[i3] += tdir;
		}

		for (int a = 0; a < vertexCount; ++a)
		{
			var n = normals[a];
			var t = tan1[a];

			var tmp = (t - n * Vector3.Dot(n, t)).normalized;
			tangents[a] = new Vector4(tmp.x, tmp.y, tmp.z);
			tangents[a].w = Vector3.Dot(Vector3.Cross(n, t), tan2[a]) < 0.0f ? -1.0f : 1.0f;
		}

		return tangents;
	}

	private int AppendGeometry(Vector3 position, IEnumerable<Vector3> vertices, IEnumerable<Vector3> normals, IEnumerable<Vector2> uvs, IEnumerable<Color> colors, IEnumerable<int> triangles)
	{
		var v = this.vertices.Count;

		foreach (var vertex in vertices)
		{
			this.vertices.Add(position + vertex);
		}

		this.colors.AddRange(colors);
		this.normals.AddRange(normals);
		this.uvs.AddRange(uvs);

		foreach (var triangle in triangles)
		{
			this.triangles.Add(v + triangle);
		}

		return v;
	}

	#endregion
}

Usage:

var points = new List<Vector3>();
for (int i = 0; i < 10; i++)
{
	points.Add(20 * new Vector3(i, 0, UnityEngine.Random.value));
}

var splineRenderer = SplineRenderer.FindInstance();
splineRenderer.AddLine(this, new Line(points, Color.green, width: 1f, segments: 10));

Result:

1 Like