using System; using Unity.Collections; using Unity.Mathematics; using UnityEngine.Rendering; namespace UnityEngine.Splines { /// <summary> /// Utility methods for creating and working with meshes. /// </summary> public static class SplineMesh { const float k_RadiusMin = .00001f, k_RadiusMax = 10000f; const int k_SidesMin = 3, k_SidesMax = 2084; const int k_SegmentsMin = 2, k_SegmentsMax = 4096; static readonly VertexAttributeDescriptor[] k_PipeVertexAttribs = new VertexAttributeDescriptor[] { new (VertexAttribute.Position), new (VertexAttribute.Normal), new (VertexAttribute.TexCoord0, dimension: 2) }; /// <summary> /// Interface for Spline mesh vertex data. Implement this interface if you are extruding custom mesh data and /// do not want to use the vertex layout provided by <see cref="SplineMesh"/>."/>. /// </summary> public interface ISplineVertexData { /// <summary> /// Vertex position. /// </summary> public Vector3 position { get; set; } /// <summary> /// Vertex normal. /// </summary> public Vector3 normal { get; set; } /// <summary> /// Vertex texture, corresponds to UV0. /// </summary> public Vector2 texture { get; set; } } [System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)] struct VertexData : ISplineVertexData { public Vector3 position { get; set; } public Vector3 normal { get; set; } public Vector2 texture { get; set; } } static void ExtrudeRing<T, K>(T spline, float t, NativeArray<K> data, int start, int count, float radius) where T : ISpline where K : struct, ISplineVertexData { var evaluationT = spline.Closed ? math.frac(t) : math.clamp(t, 0f, 1f); spline.Evaluate(evaluationT, out var sp, out var st, out _); var length = math.lengthsq(st); if (length == 0f || float.IsNaN(length)) st = spline.EvaluateTangent(math.clamp(evaluationT + (0.0001f * (t < 1f ? 1f : -1f)), 0f, 1f)); st = math.normalize(st); var rot = Quaternion.LookRotation(st); var rad = math.radians(360f / count); for (int n = 0; n < count; ++n) { var vertex = new K(); var p = new Vector3(math.cos(n * rad), math.sin(n * rad), 0f) * radius; vertex.position = (Vector3)sp + (rot * p); vertex.normal = (vertex.position - (Vector3)sp).normalized; // instead of inserting a seam, wrap UVs using a triangle wave so that texture wraps back onto itself float ut = n / ((float)count + count%2); float u = math.abs(ut - math.floor(ut + .5f)) * 2f; vertex.texture = new Vector2(u, t * spline.GetLength()); data[start + n] = vertex; } } // The logic around when caps and closing is a little complicated and easy to confuse. This wraps settings in a // consistent way so that methods aren't working with mixed data. struct Settings { public int sides { get; private set; } public int segments { get; private set; } public bool capped { get; private set; } public bool closed { get; private set; } public float2 range { get; private set; } public float radius { get; private set; } public Settings(int sides, int segments, bool capped, bool closed, float2 range, float radius) { this.sides = math.clamp(sides, k_SidesMin, k_SidesMax); this.segments = math.clamp(segments, k_SegmentsMin, k_SegmentsMax); this.range = new float2(math.min(range.x, range.y), math.max(range.x, range.y)); this.closed = math.abs(1f - (this.range.y - this.range.x)) < float.Epsilon && closed; this.capped = capped && !this.closed; this.radius = math.clamp(radius, k_RadiusMin, k_RadiusMax); } } /// <summary> /// Calculate the vertex and index count required for an extruded mesh. /// Use this method to allocate attribute and index buffers for use with Extrude. /// </summary> /// <param name="vertexCount">The number of vertices required for an extruded mesh using the provided settings.</param> /// <param name="indexCount">The number of indices required for an extruded mesh using the provided settings.</param> /// <param name="sides">How many sides make up the radius of the mesh.</param> /// <param name="segments">How many sections compose the length of the mesh.</param> /// <param name="range"> /// The section of the Spline to extrude. This value expects a normalized interpolation start and end. /// I.e., [0,1] is the entire Spline, whereas [.5, 1] is the last half of the Spline. /// </param> /// <param name="capped">Whether the start and end of the mesh is filled. This setting is ignored when spline is closed.</param> /// <param name="closed">Whether the extruded mesh is closed or open. This can be separate from the Spline.Closed value.</param> public static void GetVertexAndIndexCount(int sides, int segments, bool capped, bool closed, Vector2 range, out int vertexCount, out int indexCount) { var settings = new Settings(sides, segments, capped, closed, range, 1f); GetVertexAndIndexCount(settings, out vertexCount, out indexCount); } static void GetVertexAndIndexCount(Settings settings, out int vertexCount, out int indexCount) { vertexCount = settings.sides * (settings.segments + (settings.capped ? 2 : 0)); indexCount = settings.sides * 6 * (settings.segments - (settings.closed ? 0 : 1)) + (settings.capped ? (settings.sides - 2) * 3 * 2 : 0); } /// <summary> /// Extrude a mesh along a spline in a tube-like shape. /// </summary> /// <param name="spline">The spline to extrude.</param> /// <param name="mesh">A mesh that will be cleared and filled with vertex data for the shape.</param> /// <param name="radius">The radius of the extruded mesh.</param> /// <param name="sides">How many sides make up the radius of the mesh.</param> /// <param name="segments">How many sections compose the length of the mesh.</param> /// <param name="capped">Whether the start and end of the mesh is filled. This setting is ignored when spline is closed.</param> /// <typeparam name="T">A type implementing ISpline.</typeparam> public static void Extrude<T>(T spline, Mesh mesh, float radius, int sides, int segments, bool capped = true) where T : ISpline { Extrude(spline, mesh, radius, sides, segments, capped, new float2(0f, 1f)); } /// <summary> /// Extrude a mesh along a spline in a tube-like shape. /// </summary> /// <param name="spline">The spline to extrude.</param> /// <param name="mesh">A mesh that will be cleared and filled with vertex data for the shape.</param> /// <param name="radius">The radius of the extruded mesh.</param> /// <param name="sides">How many sides make up the radius of the mesh.</param> /// <param name="segments">How many sections compose the length of the mesh.</param> /// <param name="capped">Whether the start and end of the mesh is filled. This setting is ignored when spline is closed.</param> /// <param name="range"> /// The section of the Spline to extrude. This value expects a normalized interpolation start and end. /// I.e., [0,1] is the entire Spline, whereas [.5, 1] is the last half of the Spline. /// </param> /// <typeparam name="T">A type implementing ISpline.</typeparam> public static void Extrude<T>(T spline, Mesh mesh, float radius, int sides, int segments, bool capped, float2 range) where T : ISpline { var settings = new Settings(sides, segments, capped, spline.Closed, range, radius); GetVertexAndIndexCount(settings, out var vertexCount, out var indexCount); var meshDataArray = Mesh.AllocateWritableMeshData(1); var data = meshDataArray[0]; var indexFormat = vertexCount >= ushort.MaxValue ? IndexFormat.UInt32 : IndexFormat.UInt16; data.SetIndexBufferParams(indexCount, indexFormat); data.SetVertexBufferParams(vertexCount, k_PipeVertexAttribs); var vertices = data.GetVertexData<VertexData>(); if (indexFormat == IndexFormat.UInt16) { var indices = data.GetIndexData<UInt16>(); Extrude(spline, vertices, indices, radius, sides, segments, capped, range); } else { var indices = data.GetIndexData<UInt32>(); Extrude(spline, vertices, indices, radius, sides, segments, capped, range); } mesh.Clear(); data.subMeshCount = 1; data.SetSubMesh(0, new SubMeshDescriptor(0, indexCount)); Mesh.ApplyAndDisposeWritableMeshData(meshDataArray, mesh); mesh.RecalculateBounds(); } /// <summary> /// Extrude a mesh along a spline in a tube-like shape. /// </summary> /// <param name="spline">The spline to extrude.</param> /// <param name="vertices">A pre-allocated buffer of vertex data.</param> /// <param name="indices">A pre-allocated index buffer. Must be of type UInt16 or UInt32.</param> /// <param name="radius">The radius of the extruded mesh.</param> /// <param name="sides">How many sides make up the radius of the mesh.</param> /// <param name="segments">How many sections compose the length of the mesh.</param> /// <param name="capped">Whether the start and end of the mesh is filled. This setting is ignored when spline /// is closed.</param> /// <param name="range"> /// The section of the Spline to extrude. This value expects a normalized interpolation start and end. /// I.e., [0,1] is the entire Spline, whereas [.5, 1] is the last half of the Spline. /// </param> /// <typeparam name="TSplineType">A type implementing ISpline.</typeparam> /// <typeparam name="TVertexType">A type implementing ISplineVertexData.</typeparam> /// <typeparam name="TIndexType">The mesh index format. Must be UInt16 or UInt32.</typeparam> /// <exception cref="ArgumentOutOfRangeException">An out of range exception is thrown if the vertex or index /// buffer lengths do not match the expected size. Use <see cref="GetVertexAndIndexCount"/> to calculate the /// expected buffer sizes. /// </exception> /// <exception cref="ArgumentException"> /// An argument exception is thrown if {TIndexType} is not UInt16 or UInt32. /// </exception> public static void Extrude<TSplineType, TVertexType, TIndexType>( TSplineType spline, NativeArray<TVertexType> vertices, NativeArray<TIndexType> indices, float radius, int sides, int segments, bool capped, float2 range) where TSplineType : ISpline where TVertexType : struct, ISplineVertexData where TIndexType : struct { Extrude(spline, vertices, indices, new Settings(sides, segments, capped, spline.Closed, range, radius)); } static void Extrude<TSplineType, TVertexType, TIndexType>( TSplineType spline, NativeArray<TVertexType> vertices, NativeArray<TIndexType> indices, Settings settings) where TSplineType : ISpline where TVertexType : struct, ISplineVertexData where TIndexType : struct { var radius = settings.radius; var sides = settings.sides; var segments = settings.segments; var range = settings.range; var capped = settings.capped; var closed = settings.closed; GetVertexAndIndexCount(settings, out var vertexCount, out var indexCount); if (sides < 3) throw new ArgumentOutOfRangeException(nameof(sides), "Sides must be greater than 3"); if (segments < 2) throw new ArgumentOutOfRangeException(nameof(segments), "Segments must be greater than 2"); if (vertices.Length != vertexCount) throw new ArgumentOutOfRangeException($"Vertex array is incorrect size. Expected {vertexCount}, but received {vertices.Length}."); if (indices.Length != indexCount) throw new ArgumentOutOfRangeException($"Index array is incorrect size. Expected {indexCount}, but received {indices.Length}."); if (typeof(TIndexType) == typeof(UInt16)) { var ushortIndices = indices.Reinterpret<UInt16>(); WindTris(ushortIndices, settings); } else if (typeof(TIndexType) == typeof(UInt32)) { var ulongIndices = indices.Reinterpret<UInt32>(); WindTris(ulongIndices, settings); } else { throw new ArgumentException("Indices must be UInt16 or UInt32", nameof(indices)); } for (int i = 0; i < segments; ++i) ExtrudeRing(spline, math.lerp(range.x, range.y, i / (segments - 1f)), vertices, i * sides, sides, radius); if (capped) { var capVertexStart = segments * sides; var endCapVertexStart = (segments + 1) * sides; var rng = spline.Closed ? math.frac(range) : math.clamp(range, 0f, 1f); ExtrudeRing(spline, rng.x, vertices, capVertexStart, sides, radius); ExtrudeRing(spline, rng.y, vertices, endCapVertexStart, sides, radius); var beginAccel = math.normalize(spline.EvaluateTangent(rng.x)); var accelLen = math.lengthsq(beginAccel); if (accelLen == 0f || float.IsNaN(accelLen)) beginAccel = math.normalize(spline.EvaluateTangent(rng.x + 0.0001f)); var endAccel = math.normalize(spline.EvaluateTangent(rng.y)); accelLen = math.lengthsq(endAccel); if (accelLen == 0f || float.IsNaN(accelLen)) endAccel = math.normalize(spline.EvaluateTangent(rng.y - 0.0001f)); var rad = math.radians(360f / sides); var off = new float2(.5f, .5f); for (int i = 0; i < sides; ++i) { var v0 = vertices[capVertexStart + i]; var v1 = vertices[endCapVertexStart + i]; v0.normal = -beginAccel; v0.texture = off + new float2(math.cos(i * rad), math.sin(i * rad)) * .5f; v1.normal = endAccel; v1.texture = off + new float2(-math.cos(i * rad), math.sin(i * rad)) * .5f; vertices[capVertexStart + i] = v0; vertices[endCapVertexStart + i] = v1; } } } // Two overloads for winding triangles because there is no generic constraint for UInt{16, 32} static void WindTris(NativeArray<UInt16> indices, Settings settings) { var closed = settings.closed; var segments = settings.segments; var sides = settings.sides; var capped = settings.capped; for (int i = 0; i < (closed ? segments : segments - 1); ++i) { for (int n = 0; n < sides; ++n) { var index0 = i * sides + n; var index1 = i * sides + ((n + 1) % sides); var index2 = ((i+1) % segments) * sides + n; var index3 = ((i+1) % segments) * sides + ((n + 1) % sides); indices[i * sides * 6 + n * 6 + 0] = (UInt16) index0; indices[i * sides * 6 + n * 6 + 1] = (UInt16) index1; indices[i * sides * 6 + n * 6 + 2] = (UInt16) index2; indices[i * sides * 6 + n * 6 + 3] = (UInt16) index1; indices[i * sides * 6 + n * 6 + 4] = (UInt16) index3; indices[i * sides * 6 + n * 6 + 5] = (UInt16) index2; } } if (capped) { var capVertexStart = segments * sides; var capIndexStart = sides * 6 * (segments-1); var endCapVertexStart = (segments + 1) * sides; var endCapIndexStart = (segments-1) * 6 * sides + (sides-2) * 3; for(ushort i = 0; i < sides - 2; ++i) { indices[capIndexStart + i * 3 + 0] = (UInt16)(capVertexStart); indices[capIndexStart + i * 3 + 1] = (UInt16)(capVertexStart + i + 2); indices[capIndexStart + i * 3 + 2] = (UInt16)(capVertexStart + i + 1); indices[endCapIndexStart + i * 3 + 0] = (UInt16) (endCapVertexStart); indices[endCapIndexStart + i * 3 + 1] = (UInt16) (endCapVertexStart + i + 1); indices[endCapIndexStart + i * 3 + 2] = (UInt16) (endCapVertexStart + i + 2); } } } // Two overloads for winding triangles because there is no generic constraint for UInt{16, 32} static void WindTris(NativeArray<UInt32> indices, Settings settings) { var closed = settings.closed; var segments = settings.segments; var sides = settings.sides; var capped = settings.capped; for (int i = 0; i < (closed ? segments : segments - 1); ++i) { for (int n = 0; n < sides; ++n) { var index0 = i * sides + n; var index1 = i * sides + ((n + 1) % sides); var index2 = ((i+1) % segments) * sides + n; var index3 = ((i+1) % segments) * sides + ((n + 1) % sides); indices[i * sides * 6 + n * 6 + 0] = (UInt32) index0; indices[i * sides * 6 + n * 6 + 1] = (UInt32) index1; indices[i * sides * 6 + n * 6 + 2] = (UInt32) index2; indices[i * sides * 6 + n * 6 + 3] = (UInt32) index1; indices[i * sides * 6 + n * 6 + 4] = (UInt32) index3; indices[i * sides * 6 + n * 6 + 5] = (UInt32) index2; } } if (capped) { var capVertexStart = segments * sides; var capIndexStart = sides * 6 * (segments-1); var endCapVertexStart = (segments + 1) * sides; var endCapIndexStart = (segments-1) * 6 * sides + (sides-2) * 3; for(ushort i = 0; i < sides - 2; ++i) { indices[capIndexStart + i * 3 + 0] = (UInt32)(capVertexStart); indices[capIndexStart + i * 3 + 1] = (UInt32)(capVertexStart + i + 2); indices[capIndexStart + i * 3 + 2] = (UInt32)(capVertexStart + i + 1); indices[endCapIndexStart + i * 3 + 0] = (UInt32) (endCapVertexStart); indices[endCapIndexStart + i * 3 + 1] = (UInt32) (endCapVertexStart + i + 1); indices[endCapIndexStart + i * 3 + 2] = (UInt32) (endCapVertexStart + i + 2); } } } } }