using System;
using Unity.Collections;
using Unity.Mathematics;
using UnityEngine.Rendering;
namespace UnityEngine.Splines
{
///
/// Utility methods for creating and working with meshes.
///
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)
};
///
/// 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 ."/>.
///
public interface ISplineVertexData
{
///
/// Vertex position.
///
public Vector3 position { get; set; }
///
/// Vertex normal.
///
public Vector3 normal { get; set; }
///
/// Vertex texture, corresponds to UV0.
///
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 spline, float t, NativeArray 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);
}
}
///
/// Calculate the vertex and index count required for an extruded mesh.
/// Use this method to allocate attribute and index buffers for use with Extrude.
///
/// The number of vertices required for an extruded mesh using the provided settings.
/// The number of indices required for an extruded mesh using the provided settings.
/// How many sides make up the radius of the mesh.
/// How many sections compose the length of the mesh.
///
/// 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.
///
/// Whether the start and end of the mesh is filled. This setting is ignored when spline is closed.
/// Whether the extruded mesh is closed or open. This can be separate from the Spline.Closed value.
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);
}
///
/// Extrude a mesh along a spline in a tube-like shape.
///
/// The spline to extrude.
/// A mesh that will be cleared and filled with vertex data for the shape.
/// The radius of the extruded mesh.
/// How many sides make up the radius of the mesh.
/// How many sections compose the length of the mesh.
/// Whether the start and end of the mesh is filled. This setting is ignored when spline is closed.
/// A type implementing ISpline.
public static void Extrude(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));
}
///
/// Extrude a mesh along a spline in a tube-like shape.
///
/// The spline to extrude.
/// A mesh that will be cleared and filled with vertex data for the shape.
/// The radius of the extruded mesh.
/// How many sides make up the radius of the mesh.
/// How many sections compose the length of the mesh.
/// Whether the start and end of the mesh is filled. This setting is ignored when spline is closed.
///
/// 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.
///
/// A type implementing ISpline.
public static void Extrude(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();
if (indexFormat == IndexFormat.UInt16)
{
var indices = data.GetIndexData();
Extrude(spline, vertices, indices, radius, sides, segments, capped, range);
}
else
{
var indices = data.GetIndexData();
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();
}
///
/// Extrude a mesh along a spline in a tube-like shape.
///
/// The spline to extrude.
/// A pre-allocated buffer of vertex data.
/// A pre-allocated index buffer. Must be of type UInt16 or UInt32.
/// The radius of the extruded mesh.
/// How many sides make up the radius of the mesh.
/// How many sections compose the length of the mesh.
/// Whether the start and end of the mesh is filled. This setting is ignored when spline
/// is closed.
///
/// 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.
///
/// A type implementing ISpline.
/// A type implementing ISplineVertexData.
/// The mesh index format. Must be UInt16 or UInt32.
/// An out of range exception is thrown if the vertex or index
/// buffer lengths do not match the expected size. Use to calculate the
/// expected buffer sizes.
///
///
/// An argument exception is thrown if {TIndexType} is not UInt16 or UInt32.
///
public static void Extrude(
TSplineType spline,
NativeArray vertices,
NativeArray 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 spline,
NativeArray vertices,
NativeArray 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();
WindTris(ushortIndices, settings);
}
else if (typeof(TIndexType) == typeof(UInt32))
{
var ulongIndices = indices.Reinterpret();
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 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 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);
}
}
}
}
}