using UnityEngine; using UnityEditor; using System; using System.Collections.Generic; using System.Linq; namespace SplineMesh { /// /// A component that creates a deformed mesh from a given one along the given spline segment. /// The source mesh will always be bended along the X axis. /// It can work on a cubic bezier curve or on any interval of a given spline. /// On the given interval, the mesh can be place with original scale, stretched, or repeated. /// The resulting mesh is stored in a MeshFilter component and automaticaly updated on the next update if the spline segment change. /// [DisallowMultipleComponent] [RequireComponent(typeof(MeshFilter))] [ExecuteInEditMode] public class MeshBender : MonoBehaviour { private bool isDirty = false; private Mesh result; private bool useSpline; private Spline spline; private float intervalStart, intervalEnd; private CubicBezierCurve curve; private Dictionary sampleCache = new Dictionary(); private SourceMesh source; /// /// The source mesh to bend. /// public SourceMesh Source { get { return source; } set { if (value == source) return; SetDirty(); source = value; } } private FillingMode mode = FillingMode.StretchToInterval; /// /// The scaling mode along the spline /// public FillingMode Mode { get { return mode; } set { if (value == mode) return; SetDirty(); mode = value; } } /// /// Sets a curve along which the mesh will be bent. /// The mesh will be updated if the curve changes. /// /// The to bend the source mesh along. public void SetInterval(CubicBezierCurve curve) { if (this.curve == curve) return; if (curve == null) throw new ArgumentNullException("curve"); if (this.curve != null) { this.curve.Changed.RemoveListener(SetDirty); } this.curve = curve; spline = null; curve.Changed.AddListener(SetDirty); useSpline = false; SetDirty(); } /// /// Sets a spline's interval along which the mesh will be bent. /// If interval end is absent or set to 0, the interval goes from start to spline length. /// The mesh will be update if any of the curve changes on the spline, including curves /// outside the given interval. /// /// The to bend the source mesh along. /// Distance from the spline start to place the mesh minimum X. /// Distance from the spline start to stop deforming the source mesh. public void SetInterval(Spline spline, float intervalStart, float intervalEnd = 0) { if (this.spline == spline && this.intervalStart == intervalStart && this.intervalEnd == intervalEnd) return; if (spline == null) throw new ArgumentNullException("spline"); if (intervalStart < 0 || intervalStart >= spline.Length) { throw new ArgumentOutOfRangeException("interval start must be 0 or greater and lesser than spline length (was " + intervalStart + ")"); } if (intervalEnd != 0 && intervalEnd <= intervalStart || intervalEnd > spline.Length) { throw new ArgumentOutOfRangeException("interval end must be 0 or greater than interval start, and lesser than spline length (was " + intervalEnd + ")"); } if (this.spline != null) { // unlistening previous spline this.spline.CurveChanged.RemoveListener(SetDirty); } this.spline = spline; // listening new spline spline.CurveChanged.AddListener(SetDirty); curve = null; this.intervalStart = intervalStart; this.intervalEnd = intervalEnd; useSpline = true; SetDirty(); } private void OnEnable() { if(GetComponent().sharedMesh != null) { result = GetComponent().sharedMesh; } else { GetComponent().sharedMesh = result = new Mesh(); result.name = "Generated by " + GetType().Name; } } private void LateUpdate() { ComputeIfNeeded(); } public void ComputeIfNeeded() { if (isDirty) { Compute(); } } private void SetDirty() { isDirty = true; } /// /// Bend the mesh. This method may take time and should not be called more than necessary. /// Consider using for faster result. /// private void Compute() { isDirty = false; switch (Mode) { case FillingMode.Once: FillOnce(); break; case FillingMode.Repeat: FillRepeat(); break; case FillingMode.StretchToInterval: FillStretch(); break; } } private void OnDestroy() { if(curve != null) { curve.Changed.RemoveListener(Compute); } } /// /// The mode used by to bend meshes on the interval. /// public enum FillingMode { /// /// In this mode, source mesh will be placed on the interval by preserving mesh scale. /// Vertices that are beyond interval end will be placed on the interval end. /// Once, /// /// In this mode, the mesh will be repeated to fill the interval, preserving /// mesh scale. /// This filling process will stop when the remaining space is not enough to /// place a whole mesh, leading to an empty interval. /// Repeat, /// /// In this mode, the mesh is deformed along the X axis to fill exactly the interval. /// StretchToInterval } private void FillOnce() { sampleCache.Clear(); var bentVertices = new List(source.Vertices.Count); // for each mesh vertex, we found its projection on the curve foreach (var vert in source.Vertices) { float distance = vert.position.x - source.MinX; CurveSample sample; if (!sampleCache.TryGetValue(distance, out sample)) { if (!useSpline) { if (distance > curve.Length) distance = curve.Length; sample = curve.GetSampleAtDistance(distance); } else { float distOnSpline = intervalStart + distance; if (distOnSpline > spline.Length) { if (spline.IsLoop) { while (distOnSpline > spline.Length) { distOnSpline -= spline.Length; } } else { distOnSpline = spline.Length; } } sample = spline.GetSampleAtDistance(distOnSpline); } sampleCache[distance] = sample; } bentVertices.Add(sample.GetBent(vert)); } MeshUtility.Update(result, source.Mesh, source.Triangles, bentVertices.Select(b => b.position), bentVertices.Select(b => b.normal)); } private void FillRepeat() { float intervalLength = useSpline? (intervalEnd == 0 ? spline.Length : intervalEnd) - intervalStart : curve.Length; int repetitionCount = Mathf.FloorToInt(intervalLength / source.Length); // building triangles and UVs for the repeated mesh var triangles = new List(); var uv = new List(); var uv2 = new List(); var uv3 = new List(); var uv4 = new List(); var uv5 = new List(); var uv6 = new List(); var uv7 = new List(); var uv8 = new List(); for (int i = 0; i < repetitionCount; i++) { foreach (var index in source.Triangles) { triangles.Add(index + source.Vertices.Count * i); } uv.AddRange(source.Mesh.uv); uv2.AddRange(source.Mesh.uv2); uv3.AddRange(source.Mesh.uv3); uv4.AddRange(source.Mesh.uv4); #if UNITY_2018_2_OR_NEWER uv5.AddRange(source.Mesh.uv5); uv6.AddRange(source.Mesh.uv6); uv7.AddRange(source.Mesh.uv7); uv8.AddRange(source.Mesh.uv8); #endif } // computing vertices and normals var bentVertices = new List(source.Vertices.Count); float offset = 0; for (int i = 0; i < repetitionCount; i++) { sampleCache.Clear(); // for each mesh vertex, we found its projection on the curve foreach (var vert in source.Vertices) { float distance = vert.position.x - source.MinX + offset; CurveSample sample; if (!sampleCache.TryGetValue(distance, out sample)) { if (!useSpline) { if (distance > curve.Length) continue; sample = curve.GetSampleAtDistance(distance); } else { float distOnSpline = intervalStart + distance; //if (true) { //spline.isLoop) { while (distOnSpline > spline.Length) { distOnSpline -= spline.Length; } //} else if (distOnSpline > spline.Length) { // continue; //} sample = spline.GetSampleAtDistance(distOnSpline); } sampleCache[distance] = sample; } bentVertices.Add(sample.GetBent(vert)); } offset += source.Length; } MeshUtility.Update(result, source.Mesh, triangles, bentVertices.Select(b => b.position), bentVertices.Select(b => b.normal), uv, uv2, uv3, uv4, uv5, uv6, uv7, uv8); } private void FillStretch() { var bentVertices = new List(source.Vertices.Count); sampleCache.Clear(); // for each mesh vertex, we found its projection on the curve foreach (var vert in source.Vertices) { float distanceRate = source.Length == 0 ? 0 : Math.Abs(vert.position.x - source.MinX) / source.Length; CurveSample sample; if (!sampleCache.TryGetValue(distanceRate, out sample)) { if (!useSpline) { sample = curve.GetSampleAtDistance(curve.Length * distanceRate); } else { float intervalLength = intervalEnd == 0 ? spline.Length - intervalStart : intervalEnd - intervalStart; float distOnSpline = intervalStart + intervalLength * distanceRate; if(distOnSpline > spline.Length) { distOnSpline = spline.Length; Debug.Log("dist " + distOnSpline + " spline length " + spline.Length + " start " + intervalStart); } sample = spline.GetSampleAtDistance(distOnSpline); } sampleCache[distanceRate] = sample; } bentVertices.Add(sample.GetBent(vert)); } MeshUtility.Update(result, source.Mesh, source.Triangles, bentVertices.Select(b => b.position), bentVertices.Select(b => b.normal)); if (TryGetComponent(out MeshCollider collider)) { collider.sharedMesh = result; } } } }