├── Editor.meta ├── Editor ├── Spline2DInspector.cs └── Spline2DInspector.cs.meta ├── LICENSE ├── LICENSE.meta ├── README.md ├── README.md.meta ├── Spline2D.cs ├── Spline2D.cs.meta ├── Spline2DComponent.cs ├── Spline2DComponent.cs.meta ├── screenshot.png └── screenshot.png.meta /Editor.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 31e3a3d9a1d24a64995cd59950379aa3 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Editor/Spline2DInspector.cs: -------------------------------------------------------------------------------- 1 | using UnityEditor; 2 | using UnityEngine; 3 | 4 | [CustomEditor(typeof(Spline2DComponent))] 5 | public class Spline2DInspector : Editor { 6 | 7 | private Spline2DComponent spline; 8 | private const float handleSize = 0.04f; 9 | private const float pickSize = 0.06f; 10 | private int selectedIndex = -1; 11 | 12 | public override void OnInspectorGUI () { 13 | spline = target as Spline2DComponent; 14 | 15 | EditorGUILayout.Space(); 16 | GUILayout.BeginHorizontal(); 17 | if (GUILayout.Button("Add Point")) { 18 | Undo.RecordObject(spline, "Add Point"); 19 | AddNewPoint(); 20 | } 21 | 22 | if (selectedIndex == -1) { 23 | GUI.enabled = false; 24 | } 25 | if (GUILayout.Button("Insert Point")) { 26 | Undo.RecordObject(spline, "Insert Point"); 27 | InsertNewPoint(); 28 | } 29 | if (GUILayout.Button("Remove Point")) { 30 | Undo.RecordObject(spline, "Remove Point"); 31 | RemovePoint(); 32 | } 33 | GUILayout.EndHorizontal(); 34 | GUI.enabled = true; 35 | 36 | EditorGUILayout.Space(); 37 | EditorGUI.indentLevel += 2; 38 | DrawSelectedPointInspector(); 39 | EditorGUI.indentLevel -= 2; 40 | 41 | EditorGUILayout.Space(); 42 | // DON'T use the default inspector, it will bypass setters and we need 43 | // those to be called to properly dirty the state 44 | EditorGUI.BeginChangeCheck(); 45 | bool closed = EditorGUILayout.Toggle("Closed", spline.IsClosed); 46 | if (EditorGUI.EndChangeCheck()) { 47 | Undo.RecordObject(spline, "Toggle Closed"); 48 | spline.IsClosed = closed; 49 | } 50 | EditorGUI.BeginChangeCheck(); 51 | bool xz = EditorGUILayout.Toggle("X/Z Mode", spline.displayXZ); 52 | if (EditorGUI.EndChangeCheck()) { 53 | Undo.RecordObject(spline, "Toggle XZ Mode"); 54 | spline.displayXZ = xz; 55 | } 56 | EditorGUI.BeginChangeCheck(); 57 | float curve = EditorGUILayout.FloatField("Curvature", spline.Curvature); 58 | if (EditorGUI.EndChangeCheck()) { 59 | Undo.RecordObject(spline, "Set Curvature"); 60 | spline.Curvature = curve; 61 | } 62 | EditorGUI.BeginChangeCheck(); 63 | int lenSamples = EditorGUILayout.IntSlider("Length Sampling", spline.LengthSamplesPerSegment, 1, 20); 64 | if (EditorGUI.EndChangeCheck()) { 65 | Undo.RecordObject(spline, "Set Length Sampling"); 66 | spline.LengthSamplesPerSegment = lenSamples; 67 | } 68 | 69 | 70 | EditorGUILayout.Space(); 71 | EditorGUILayout.BeginHorizontal(); 72 | EditorGUI.BeginChangeCheck(); 73 | bool showDistance = EditorGUILayout.Toggle("Show Distance", spline.showDistance); 74 | if (EditorGUI.EndChangeCheck()) { 75 | Undo.RecordObject(spline, "Toggle Show Distance"); 76 | spline.showDistance = showDistance; 77 | } 78 | EditorGUI.BeginChangeCheck(); 79 | float dist = EditorGUILayout.FloatField("Interval", spline.distanceMarker); 80 | if (EditorGUI.EndChangeCheck()) { 81 | Undo.RecordObject(spline, "Set Distance Interval"); 82 | spline.distanceMarker = dist; 83 | } 84 | EditorGUILayout.EndHorizontal(); 85 | 86 | EditorGUILayout.BeginHorizontal(); 87 | EditorGUI.BeginChangeCheck(); 88 | bool showNormals = EditorGUILayout.Toggle("Show Normals", spline.showNormals); 89 | if (EditorGUI.EndChangeCheck()) { 90 | Undo.RecordObject(spline, "Toggle Show Normals"); 91 | spline.showNormals = showNormals; 92 | } 93 | 94 | EditorGUI.BeginChangeCheck(); 95 | float nmlen = EditorGUILayout.FloatField("Length", spline.normalDisplayLength); 96 | if (EditorGUI.EndChangeCheck()) { 97 | Undo.RecordObject(spline, "Set Normal Display Length"); 98 | spline.normalDisplayLength = nmlen; 99 | } 100 | EditorGUILayout.EndHorizontal(); 101 | 102 | } 103 | 104 | private void RemovePoint() { 105 | spline.RemovePoint(selectedIndex); 106 | if (selectedIndex > 0) { 107 | --selectedIndex; 108 | } else if (spline.Count == 0) { 109 | selectedIndex = -1; 110 | } 111 | 112 | } 113 | 114 | private void AddNewPoint() { 115 | // First point at zero 116 | Vector2 pos = Vector2.zero; 117 | if (spline.Count == 1) { 118 | // Second point up & right 2 units 119 | pos = spline.GetPoint(0) + Vector2.up * 2.0f + Vector2.right * 2.0f; 120 | } else if (spline.Count > 1) { 121 | // Third+ point extended from previous & varied a little 122 | // rotate left/right alternately for interest 123 | Vector2 endpos = spline.GetPoint(spline.Count-1); 124 | Vector2 diff = endpos - spline.GetPoint(spline.Count-2); 125 | float angle = spline.Count % 2 > 0 ? 30.0f : -30.0f; 126 | diff = Quaternion.AngleAxis(angle, Vector3.forward) * diff; 127 | pos = endpos + diff; 128 | } 129 | spline.AddPoint(pos); 130 | 131 | } 132 | 133 | private void InsertNewPoint() { 134 | if (selectedIndex < 0 || selectedIndex >= spline.Count) 135 | return; 136 | 137 | if (spline.Count == 0) { 138 | AddNewPoint(); 139 | } else { 140 | Vector2 pos = Vector2.zero; 141 | if (selectedIndex == 0) { 142 | // Insert at start 143 | // If nothing to project, just down & left 144 | if (spline.Count == 1) 145 | pos = spline.GetPoint(0) - Vector2.up * 2.0f - Vector2.right * 2.0f; 146 | else { 147 | // Extended from previous & varied a little 148 | // rotate left/right alternately for interest 149 | Vector2 tangent = spline.Derivative(0); 150 | pos = spline.GetPoint(0) - tangent * 2f; 151 | } 152 | 153 | } else { 154 | // split half way between previous and selected 155 | pos = spline.Interpolate(selectedIndex-1, 0.5f); 156 | } 157 | 158 | spline.InsertPoint(selectedIndex, pos); 159 | } 160 | } 161 | 162 | 163 | 164 | private void DrawSelectedPointInspector() { 165 | if (selectedIndex == -1) { 166 | EditorGUILayout.LabelField("Selected Point: None"); 167 | } else { 168 | EditorGUI.BeginChangeCheck(); 169 | Vector2 point = EditorGUILayout.Vector2Field("Selected Point", spline.GetPoint(selectedIndex)); 170 | if (EditorGUI.EndChangeCheck()) { 171 | Undo.RecordObject(spline, "Move Point"); 172 | spline.SetPoint(selectedIndex, point); 173 | } 174 | } 175 | } 176 | 177 | private void OnSceneGUI () { 178 | spline = target as Spline2DComponent; 179 | 180 | DrawPoints(); 181 | } 182 | 183 | private void DrawPoints() { 184 | if (spline.Count == 0) { 185 | return; 186 | } 187 | for (int i = 0; i < spline.Count; ++i) { 188 | ShowPoint(i); 189 | } 190 | } 191 | 192 | private void ShowPoint (int index) { 193 | Vector3 point = spline.GetPointWorldSpace(index); 194 | float size = HandleUtility.GetHandleSize(point); 195 | if (index == 0) { 196 | Handles.color = Color.green; 197 | } else if (index == spline.Count - 1 && !spline.IsClosed) { 198 | Handles.color = Color.red; 199 | } else { 200 | Handles.color = Color.cyan; 201 | } 202 | 203 | if (Handles.Button(point, Quaternion.identity, size * handleSize, size * pickSize, Handles.DotCap)) { 204 | selectedIndex = index; 205 | Repaint(); 206 | } 207 | if (selectedIndex == index) { 208 | EditorGUI.BeginChangeCheck(); 209 | point = Handles.FreeMoveHandle(point, Quaternion.identity, size * 1.5f * handleSize, Vector3.zero, Handles.CircleCap); 210 | if (EditorGUI.EndChangeCheck()) { 211 | Undo.RecordObject(spline, "Move Point"); 212 | EditorUtility.SetDirty(spline); 213 | spline.SetPointWorldSpace(index, point); 214 | } 215 | } 216 | } 217 | 218 | } 219 | 220 | -------------------------------------------------------------------------------- /Editor/Spline2DInspector.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: b33f99c40f5d18044b7dfe86a59642e2 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Steve Streeting 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /LICENSE.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e059421b4afdfca4996b982443a05c6a 3 | DefaultImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Unity Spline2D 2 | 3 | > #### Note: I have stopped using Unity and so am not maintaining this library any more. 4 | 5 | Unity Spline2D is a free 2D simple spline library for Unity. 6 | 7 | ![Screenshot](screenshot.png) 9 | 10 | ## Features 11 | 12 | * **Easy to use**: define a series of intersection points and that's it. The spline 13 | will pass through those points with a default Catmull-Rom curvature. No 14 | need to set tangent control points. 15 | * **Positions and Derivatives**: get both a position on the curve and its 16 | derivative (to get facing) 17 | * **Interpolate by Distance**: interpolating by `t` alone can result in varied 18 | speeds along the curve if your points are not equidistant. With Spline2D you 19 | can ask for a position a specific distance along the curve instead, and therefore 20 | traverse the curve at a known speed regardless of the structure of the curve. 21 | * **Tweakable curvature**: if you want something other than Catmull-Rom style 22 | curves, you can increase/decrease the `Curvature` property to change the shape 23 | * **Optimised for 2D**: always flat, defaults to X/Y like 2D mode in Unity 24 | but can be switched to display/edit in X/Z for use with 3D floor planes 25 | 26 | ## Installation 27 | * Place `Spline2D.cs` and `Spline2DComponent.cs` anywhere you like 28 | * Place `Spline2DInspector.cs` in an `Editor` subfolder somewhere. 29 | 30 | ## Usage: 31 | 32 | * To just use splines from general code, instantiate `Spline2D` and call its 33 | AddPoint methods etc to build the spline 34 | * To use a spline in a scene & save it, use `Spline2DComponent` as a regular 35 | Unity component. There is a inspector with buttons to add/remove points, and 36 | you can select points and drag them around to change the curve. 37 | 38 | ## License 39 | 40 | The MIT License (MIT) 41 | 42 | Copyright (c) 2016-2019 Steve Streeting 43 | 44 | Permission is hereby granted, free of charge, to any person obtaining a copy 45 | of this software and associated documentation files (the "Software"), to deal 46 | in the Software without restriction, including without limitation the rights 47 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 48 | copies of the Software, and to permit persons to whom the Software is 49 | furnished to do so, subject to the following conditions: 50 | 51 | The above copyright notice and this permission notice shall be included in all 52 | copies or substantial portions of the Software. 53 | 54 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 55 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 56 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 57 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 58 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 59 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 60 | SOFTWARE. 61 | 62 | ## Notes 63 | * Tested in Unity 5.4 - 2018 64 | * Pull requests welcome! 65 | 66 | [Spline2D]: https://github.com/sinbad/UnitySpline2D 67 | -------------------------------------------------------------------------------- /README.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 10a710e0a367b25479740422bd79ee3f 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Spline2D.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | using UnityEngine.Assertions; 5 | 6 | // Utility class for calculating a Cubic multi-segment (Hermite) spline in 2D 7 | // Hermite splines are convenient because they only need 2 positions and 2 8 | // tangents per segment, which can be automatically calculated from the surrounding 9 | // points if desired 10 | // The spline can be extended dynamically over time and must always consist of 11 | // 3 or more points. If the spline is closed, the spline will loop back to the 12 | // first point. 13 | // It can provide positions, derivatives (slope of curve) either at a parametric 14 | // 't' value over the whole curve, or as a function of distance along the curve 15 | // for constant-speed traversal. The distance is calculated approximately via 16 | // sampling (cheap integration), its accuracy is determined by LengthSamplesPerSegment 17 | // which defaults to 5 (a decent trade-off for most cases). 18 | // This object is not a MonoBehaviour to keep it flexible. If you want to 19 | // save/display one in a scene, use the wrapper Spline2DComponent class. 20 | public class Spline2D { 21 | private bool tangentsDirty = true; 22 | private bool lenSampleDirty = true; 23 | // Points which the curve passes through. 24 | private List points = new List(); 25 | // Tangents at each point; automatically calculated 26 | private List tangents = new List(); 27 | private bool closed; 28 | /// Whether the spline is closed; if so, the first point is also the last 29 | public bool IsClosed { 30 | get { return closed; } 31 | set { 32 | closed = value; 33 | tangentsDirty = true; 34 | lenSampleDirty = true; 35 | } 36 | } 37 | private float curvature = 0.5f; 38 | /// The amount of curvature in the spline; 0.5 is Catmull-Rom 39 | public float Curvature { 40 | get { return curvature; } 41 | set { 42 | curvature = value; 43 | tangentsDirty = true; 44 | lenSampleDirty = true; 45 | } 46 | } 47 | private int lengthSamplesPerSegment = 5; 48 | /// Accuracy of sampling curve to traverse by distance 49 | public int LengthSamplesPerSegment { 50 | get { return lengthSamplesPerSegment; } 51 | set { 52 | lengthSamplesPerSegment = value; 53 | lenSampleDirty = true; 54 | } 55 | } 56 | 57 | private struct DistanceToT { 58 | public float distance; 59 | public float t; 60 | public DistanceToT(float dist, float tm) { 61 | distance = dist; 62 | t = tm; 63 | } 64 | } 65 | private List distanceToTList = new List(); 66 | 67 | /// Get point count 68 | public int Count { 69 | get { return points.Count; } 70 | } 71 | 72 | /// Return the approximate length of the curve, as derived by sampling the 73 | /// curve at a resolution of LengthSamplesPerSegment 74 | public float Length { 75 | get { 76 | Recalculate(true); 77 | 78 | if (distanceToTList.Count == 0) 79 | return 0.0f; 80 | 81 | return distanceToTList[distanceToTList.Count-1].distance; 82 | } 83 | } 84 | 85 | 86 | 87 | public Spline2D() { 88 | } 89 | 90 | public Spline2D(List intersectionPoints, bool isClosed = false, float curve = 0.5f, 91 | int samplesPerSegment = 5) { 92 | points = intersectionPoints; 93 | closed = isClosed; 94 | curvature = curve; 95 | lengthSamplesPerSegment = samplesPerSegment; 96 | tangentsDirty = true; 97 | lenSampleDirty = true; 98 | } 99 | 100 | /// Add a point to the curve 101 | public void AddPoint(Vector2 p) { 102 | points.Add(p); 103 | tangentsDirty = true; 104 | lenSampleDirty = true; 105 | } 106 | 107 | /// Add a point to the curve by dropping the earliest point and scrolling 108 | /// all other points backwards 109 | /// This allows you to maintain a fixed-size spline which you extend to new 110 | /// points at the expense of dropping earliest points. This is efficient for 111 | /// unbounded paths you need to keep adding to but don't need the old history 112 | /// Note that when you do this the distances change to being measured from 113 | /// the new start point so you have to adjust your next interpolation request 114 | /// to take this into account. Subtract DistanceAtPoint(1) from distances 115 | /// before calling this method, for example (or for plain `t` interpolation, 116 | /// reduce `t` by 1f/Count) 117 | /// This method cannot be used on closed splines 118 | public void AddPointScroll(Vector2 p) { 119 | Assert.IsFalse(closed, "Cannot use AddPointScroll on closed splines!"); 120 | 121 | if (points.Count == 0) { 122 | AddPoint(p); 123 | } else { 124 | for (int i = 0; i < points.Count - 1; ++i) { 125 | points[i] = points[i+1]; 126 | } 127 | points[points.Count-1] = p; 128 | } 129 | tangentsDirty = true; 130 | lenSampleDirty = true; 131 | } 132 | 133 | /// Add a list of points to the end of the spline, in order 134 | public void AddPoints(IEnumerable plist) { 135 | points.AddRange(plist); 136 | tangentsDirty = true; 137 | lenSampleDirty = true; 138 | } 139 | 140 | /// Replace all the points in the spline from fromIndex onwards with a new set 141 | public void ReplacePoints(IEnumerable plist, int fromIndex = 0) { 142 | Assert.IsTrue(fromIndex < points.Count, "Spline2D: point index out of range"); 143 | 144 | points.RemoveRange(fromIndex, points.Count-fromIndex); 145 | points.AddRange(plist); 146 | tangentsDirty = true; 147 | lenSampleDirty = true; 148 | } 149 | 150 | /// Change a point on the curve 151 | public void SetPoint(int index, Vector2 p) { 152 | Assert.IsTrue(index < points.Count, "Spline2D: point index out of range"); 153 | 154 | points[index] = p; 155 | tangentsDirty = true; 156 | lenSampleDirty = true; 157 | } 158 | /// Remove a point on the curve 159 | public void RemovePoint(int index) { 160 | Assert.IsTrue(index < points.Count, "Spline2D: point index out of range"); 161 | 162 | points.RemoveAt(index); 163 | tangentsDirty = true; 164 | lenSampleDirty = true; 165 | } 166 | 167 | /// Insert a point on the curve before the given index 168 | public void InsertPoint(int index, Vector2 p) { 169 | Assert.IsTrue(index <= points.Count && index >= 0, "Spline2D: point index out of range"); 170 | points.Insert(index, p); 171 | tangentsDirty = true; 172 | lenSampleDirty = true; 173 | } 174 | 175 | // TODO add more efficient 'scrolling' curve of N length where we add one & 176 | // drop the earliest for effcient non-closed curves that continuously extend 177 | 178 | /// Reset & start again 179 | public void Clear() { 180 | points.Clear(); 181 | tangentsDirty = true; 182 | lenSampleDirty = true; 183 | } 184 | /// Get a single point 185 | public Vector2 GetPoint(int index) { 186 | Assert.IsTrue(index < points.Count, "Spline2D: point index out of range"); 187 | 188 | return points[index]; 189 | } 190 | 191 | 192 | 193 | /// Interpolate a position on the entire curve. Note that if the control 194 | /// points are not evenly spaced, this may result in varying speeds. 195 | public Vector2 Interpolate(float t) { 196 | Recalculate(false); 197 | 198 | int segIdx; 199 | float tSeg; 200 | ToSegment(t, out segIdx, out tSeg); 201 | 202 | return Interpolate(segIdx, tSeg); 203 | } 204 | 205 | private void ToSegment(float t, out int iSeg, out float tSeg) { 206 | // Work out which segment this is in 207 | // Closed loops have 1 extra node at t=1.0 ie the first node 208 | float pointCount = closed ? points.Count : points.Count - 1; 209 | float fSeg = t * pointCount; 210 | iSeg = (int)fSeg; 211 | // Remainder t 212 | tSeg = fSeg - iSeg; 213 | } 214 | 215 | /// Interpolate a position between one point on the curve and the next 216 | /// Rather than interpolating over the entire curve, this simply interpolates 217 | /// between the point with fromIndex and the next point 218 | public Vector2 Interpolate(int fromIndex, float t) { 219 | Recalculate(false); 220 | 221 | int toIndex = fromIndex + 1; 222 | // At or beyond last index? 223 | if (toIndex >= points.Count) { 224 | if (closed) { 225 | // Wrap 226 | toIndex = toIndex % points.Count; 227 | fromIndex = fromIndex % points.Count; 228 | } else { 229 | // Clamp to end 230 | return points[points.Count-1]; 231 | } 232 | } 233 | 234 | // Fast special cases 235 | if (Mathf.Approximately(t, 0.0f)) { 236 | return points[fromIndex]; 237 | } else if (Mathf.Approximately(t, 1.0f)) { 238 | return points[toIndex]; 239 | } 240 | 241 | // Now general case 242 | // Pre-calculate powers 243 | float t2 = t*t; 244 | float t3 = t2*t; 245 | // Calculate hermite basis parts 246 | float h1 = 2f*t3 - 3f*t2 + 1f; 247 | float h2 = -2f*t3 + 3f*t2; 248 | float h3 = t3 - 2f*t2 + t; 249 | float h4 = t3 - t2; 250 | 251 | return h1 * points[fromIndex] + 252 | h2 * points[toIndex] + 253 | h3 * tangents[fromIndex] + 254 | h4 * tangents[toIndex]; 255 | 256 | 257 | } 258 | 259 | /// Get derivative of the curve at a point. Note that if the control 260 | /// points are not evenly spaced, this may result in varying speeds. 261 | /// This is not normalised by default in case you don't need that 262 | public Vector2 Derivative(float t) { 263 | Recalculate(false); 264 | 265 | int segIdx; 266 | float tSeg; 267 | ToSegment(t, out segIdx, out tSeg); 268 | 269 | return Derivative(segIdx, tSeg); 270 | } 271 | 272 | /// Get derivative of curve between one point on the curve and the next 273 | /// Rather than interpolating over the entire curve, this simply interpolates 274 | /// between the point with fromIndex and the next segment 275 | /// This is not normalised by default in case you don't need that 276 | public Vector2 Derivative(int fromIndex, float t) { 277 | Recalculate(false); 278 | 279 | int toIndex = fromIndex + 1; 280 | // At or beyond last index? 281 | if (toIndex >= points.Count) { 282 | if (closed) { 283 | // Wrap 284 | toIndex = toIndex % points.Count; 285 | fromIndex = fromIndex % points.Count; 286 | } else { 287 | // Clamp to end 288 | toIndex = fromIndex; 289 | } 290 | } 291 | 292 | // Pre-calculate power 293 | float t2 = t*t; 294 | // Derivative of hermite basis parts 295 | float h1 = 6f*t2 - 6f*t; 296 | float h2 = -6f*t2 + 6f*t; 297 | float h3 = 3f*t2 - 4f*t + 1; 298 | float h4 = 3f*t2 - 2f*t; 299 | 300 | return h1 * points[fromIndex] + 301 | h2 * points[toIndex] + 302 | h3 * tangents[fromIndex] + 303 | h4 * tangents[toIndex]; 304 | 305 | 306 | } 307 | 308 | /// Convert a physical distance to a t position on the curve. This is 309 | /// approximate, the accuracy of can be changed via LengthSamplesPerSegment 310 | public float DistanceToLinearT(float dist) { 311 | int i; 312 | return DistanceToLinearT(dist, out i); 313 | } 314 | 315 | /// Convert a physical distance to a t position on the curve. This is 316 | /// approximate, the accuracy of can be changed via LengthSamplesPerSegment 317 | /// Also returns an out param of the last point index passed 318 | public float DistanceToLinearT(float dist, out int lastIndex) { 319 | Recalculate(true); 320 | 321 | if (distanceToTList.Count == 0) { 322 | lastIndex = 0; 323 | return 0.0f; 324 | } 325 | 326 | // Check to see if distance > length 327 | float len = Length; 328 | if (dist >= len) { 329 | if (closed) { 330 | // wrap and continue as usual 331 | dist = dist % len; 332 | } else { 333 | // clamp to end 334 | lastIndex = points.Count - 1; 335 | return 1.0f; 336 | } 337 | } 338 | 339 | 340 | float prevDist = 0.0f; 341 | float prevT = 0.0f; 342 | for (int i = 0; i < distanceToTList.Count; ++i) { 343 | DistanceToT distToT = distanceToTList[i]; 344 | if (dist < distToT.distance) { 345 | float distanceT = Mathf.InverseLerp(prevDist, distToT.distance, dist); 346 | lastIndex = i / lengthSamplesPerSegment; // not i-1 because distanceToTList starts at point index 1 347 | return Mathf.Lerp(prevT, distToT.t, distanceT); 348 | } 349 | prevDist = distToT.distance; 350 | prevT = distToT.t; 351 | } 352 | 353 | // If we got here then we ran off the end 354 | lastIndex = points.Count - 1; 355 | return 1.0f; 356 | } 357 | 358 | /// Interpolate a position on the entire curve based on distance. This is 359 | /// approximate, the accuracy of can be changed via LengthSamplesPerSegment 360 | public Vector2 InterpolateDistance(float dist) { 361 | float t = DistanceToLinearT(dist); 362 | return Interpolate(t); 363 | } 364 | 365 | /// Get derivative of the curve at a point long the curve at a distance. This 366 | /// is approximate, the accuracy of this can be changed via 367 | /// LengthSamplesPerSegment 368 | public Vector2 DerivativeDistance(float dist) { 369 | float t = DistanceToLinearT(dist); 370 | return Derivative(t); 371 | } 372 | 373 | /// Get the distance at a point index 374 | public float DistanceAtPoint(int index) { 375 | Assert.IsTrue(index < points.Count, "Spline2D: point index out of range"); 376 | 377 | // Length samples are from first actual distance, with points at 378 | // LengthSamplesPerSegment intervals 379 | if (index == 0) { 380 | return 0.0f; 381 | } 382 | Recalculate(true); 383 | return distanceToTList[index*lengthSamplesPerSegment - 1].distance; 384 | } 385 | 386 | private void Recalculate(bool includingLength) { 387 | if (tangentsDirty) { 388 | recalcTangents(); 389 | tangentsDirty = false; 390 | } 391 | // Need to check the length of distanceToTList because for some reason 392 | // when scripts are reloaded in the editor, tangents survives but 393 | // distanceToTList does not (and dirty flags remain false). Maybe because 394 | // it's a custom struct it can't be restored 395 | if (includingLength && 396 | (lenSampleDirty || distanceToTList.Count == 0)) { 397 | recalcLength(); 398 | lenSampleDirty = false; 399 | } 400 | } 401 | 402 | private void recalcTangents() { 403 | int numPoints = points.Count; 404 | if (numPoints < 2) { 405 | // Nothing to do here 406 | return; 407 | } 408 | tangents.Clear(); 409 | tangents.Capacity = numPoints; 410 | 411 | for (int i = 0; i < numPoints; ++i) { 412 | Vector2 tangent; 413 | if (i == 0) { 414 | // Special case start 415 | if (closed) { 416 | // Wrap around 417 | tangent = makeTangent(points[numPoints-1], points[1]); 418 | } else { 419 | // starting tangent is just from start to point 1 420 | tangent = makeTangent(points[i], points[i+1]); 421 | } 422 | } else if (i == numPoints-1) { 423 | // Special case end 424 | if (closed) { 425 | // Wrap around 426 | tangent = makeTangent(points[i-1], points[0]); 427 | } else { 428 | // end tangent just from prev point to end point 429 | tangent = makeTangent(points[i-1], points[i]); 430 | } 431 | } else { 432 | // Mid point is average of previous point and next point 433 | tangent = makeTangent(points[i-1], points[i+1]); 434 | } 435 | tangents.Add(tangent); 436 | } 437 | } 438 | 439 | private Vector2 makeTangent(Vector2 p1, Vector2 p2) { 440 | return curvature * (p2 - p1); 441 | } 442 | 443 | private void recalcLength() { 444 | int numPoints = points.Count; 445 | if (numPoints < 2) { 446 | // Nothing to do here 447 | return; 448 | } 449 | // Sample along curve & build distance -> t lookup, can interpolate t 450 | // linearly between nearest points to approximate distance parametrisation 451 | // count is segments * lengthSamplesPerSegment 452 | // We sample from for st t > 0 all the way to t = 1 453 | // For a closed loop, t = 1 is the first point again, for open its the last point 454 | int samples = lengthSamplesPerSegment * (closed ? points.Count : points.Count-1); 455 | 456 | distanceToTList.Clear(); 457 | distanceToTList.Capacity = samples; 458 | float distanceSoFar = 0.0f; 459 | float tinc = 1.0f / (float)samples; 460 | float t = tinc; // we don't start at 0 since that's easy 461 | Vector2 lastPos = points[0]; 462 | for (int i = 1; i <= samples; ++i) { 463 | Vector2 pos = Interpolate(t); 464 | float distInc = Vector2.Distance(lastPos, pos); 465 | distanceSoFar += distInc; 466 | distanceToTList.Add(new DistanceToT(distanceSoFar, t)); 467 | lastPos = pos; 468 | t += tinc; 469 | } 470 | } 471 | 472 | } -------------------------------------------------------------------------------- /Spline2D.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 906dacc98060fa34fb2e1944ea1b08c5 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Spline2DComponent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | 5 | // Utility class for displaying a Spline2D in a scene as a component 6 | // All the real work is done by Spline2D, this just makes is serialize and render 7 | // editable gizmos (also see Spline2DInspector) 8 | public class Spline2DComponent : MonoBehaviour { 9 | private Spline2D spline; 10 | 11 | [Tooltip("Display in the XZ plane in the editor instead of the default XY plane (spline is still in XY)")] 12 | public bool displayXZ; 13 | 14 | private void InitSpline() { 15 | if (spline == null) { 16 | spline = new Spline2D(points, closed, curvature, lengthSamplesPerSegment); 17 | } 18 | } 19 | 20 | // All state is duplicated so it can be correctly serialized, Unity never 21 | // calls getters/setters on load/save so they're useless, we have to store 22 | // actual fields here. In the editor, a custom inspector is used to ensure 23 | // the property setters are called to sync the underlying Spline2D 24 | 25 | // Points which the curve passes through. 26 | [SerializeField] 27 | private List points = new List(); 28 | [SerializeField] 29 | private bool closed; 30 | /// Whether the spline is closed; if so, the first point is also the last 31 | public bool IsClosed { 32 | get { return closed; } 33 | set { 34 | closed = value; 35 | InitSpline(); 36 | spline.IsClosed = closed; 37 | } 38 | } 39 | [SerializeField] 40 | private float curvature = 0.5f; 41 | /// The amount of curvature in the spline; 0.5 is Catmull-Rom 42 | public float Curvature { 43 | get { return curvature; } 44 | set { 45 | curvature = value; 46 | InitSpline(); 47 | spline.Curvature = curvature; 48 | } 49 | } 50 | [SerializeField] 51 | private int lengthSamplesPerSegment = 5; 52 | /// Accuracy of sampling curve to traverse by distance 53 | public int LengthSamplesPerSegment { 54 | get { return lengthSamplesPerSegment; } 55 | set { 56 | lengthSamplesPerSegment = value; 57 | InitSpline(); 58 | spline.LengthSamplesPerSegment = lengthSamplesPerSegment; 59 | } 60 | } 61 | 62 | // For gizmo drawing 63 | private const int stepsPerSegment = 20; 64 | public bool showNormals; 65 | public float normalDisplayLength = 5.0f; 66 | public bool showDistance; 67 | public float distanceMarker = 1.0f; 68 | 69 | 70 | /// Get point count 71 | public int Count { 72 | get { return points.Count; } 73 | } 74 | 75 | /// Return the approximate length of the curve, as derived by sampling the 76 | /// curve at a resolution of LengthSamplesPerSegment 77 | public float Length { 78 | get { 79 | InitSpline(); 80 | return spline.Length; 81 | } 82 | } 83 | 84 | /// Add a point to the curve (local 2D space) 85 | public void AddPoint(Vector2 p) { 86 | // We share the same list so adding there adds here 87 | InitSpline(); 88 | spline.AddPoint(p); 89 | } 90 | 91 | /// Add a point to the curve based on a world space position 92 | /// If point is off the plane of the spline it will be projected back on to it 93 | public void AddPointWorldSpace(Vector3 p) { 94 | Vector3 localP = transform.InverseTransformPoint(p); 95 | if (displayXZ) 96 | localP = FlipXZtoXY(localP); 97 | AddPoint(localP); 98 | } 99 | 100 | /// Change a point on the curve (local 2D space) 101 | public void SetPoint(int index, Vector2 p) { 102 | // We share the same list so changing there adds here 103 | InitSpline(); 104 | spline.SetPoint(index, p); 105 | } 106 | 107 | /// Change a point on the curve based on a world space position 108 | /// If point is off the plane of the spline it will be projected back on to it 109 | public void SetPointWorldSpace(int index, Vector3 p) { 110 | Vector3 localP = transform.InverseTransformPoint(p); 111 | if (displayXZ) 112 | localP = FlipXZtoXY(localP); 113 | SetPoint(index, localP); 114 | } 115 | 116 | /// Insert a point before the given index, in local 2D space 117 | public void InsertPoint(int index, Vector2 p) { 118 | // We share the same list so adding there adds here 119 | InitSpline(); 120 | spline.InsertPoint(index, p); 121 | } 122 | 123 | /// Insert a point before the given index, in world space 124 | /// If point is off the plane of the spline it will be projected back on to it 125 | public void InsertPointWorldSpace(int index, Vector3 p) { 126 | Vector3 localP = transform.InverseTransformPoint(p); 127 | if (displayXZ) 128 | localP = FlipXZtoXY(localP); 129 | InsertPoint(index, localP); 130 | } 131 | 132 | // Remove a point on the curve 133 | public void RemovePoint(int index) { 134 | // We share the same list so changing there adds here 135 | InitSpline(); 136 | spline.RemovePoint(index); 137 | } 138 | 139 | // TODO add more efficient 'scrolling' curve of N length where we add one & 140 | // drop the earliest for effcient non-closed curves that continuously extend 141 | 142 | /// Reset & start again 143 | public void Clear() { 144 | // We share the same list so changing there adds here 145 | InitSpline(); 146 | spline.Clear(); 147 | } 148 | /// Get a single point in local 2D space 149 | public Vector2 GetPoint(int index) { 150 | InitSpline(); 151 | return spline.GetPoint(index); 152 | } 153 | /// Get a single point in world space 154 | public Vector3 GetPointWorldSpace(int index) { 155 | Vector3 p = GetPoint(index); 156 | if (displayXZ) 157 | p = FlipXYtoXZ(p); 158 | return transform.TransformPoint(p); 159 | } 160 | 161 | 162 | 163 | /// Interpolate a position on the entire curve in local 2D space. Note that if the control 164 | /// points are not evenly spaced, this may result in varying speeds. 165 | public Vector2 Interpolate(float t) { 166 | InitSpline(); 167 | return spline.Interpolate(t); 168 | } 169 | 170 | /// Interpolate a position on the entire curve in world space. Note that if the control 171 | /// points are not evenly spaced, this may result in varying speeds. 172 | public Vector3 InterpolateWorldSpace(float t) { 173 | Vector3 p = Interpolate(t); 174 | if (displayXZ) 175 | p = FlipXYtoXZ(p); 176 | return transform.TransformPoint(p); 177 | } 178 | 179 | /// Interpolate a position between one point on the curve and the next 180 | /// in local 2D space. 181 | /// Rather than interpolating over the entire curve, this simply interpolates 182 | /// between the point with fromIndex and the next point 183 | public Vector2 Interpolate(int fromIndex, float t) { 184 | InitSpline(); 185 | return spline.Interpolate(fromIndex, t); 186 | } 187 | 188 | /// Interpolate a position between one point on the curve and the next 189 | /// in world space. 190 | /// Rather than interpolating over the entire curve, this simply interpolates 191 | /// between the point with fromIndex and the next point 192 | public Vector3 InterpolateWorldSpace(int fromIndex, float t) { 193 | Vector3 p = Interpolate(fromIndex, t); 194 | if (displayXZ) 195 | p = FlipXYtoXZ(p); 196 | return transform.TransformPoint(p); 197 | } 198 | 199 | /// Get derivative of the curve at a point in local 2D space. Note that if the control 200 | /// points are not evenly spaced, this may result in varying speeds. 201 | /// This is not normalised by default in case you don't need that 202 | public Vector2 Derivative(float t) { 203 | InitSpline(); 204 | return spline.Derivative(t); 205 | } 206 | 207 | /// Get derivative of the curve at a point in world space. Note that if the control 208 | /// points are not evenly spaced, this may result in varying speeds. 209 | /// This is not normalised by default in case you don't need that 210 | public Vector3 DerivativeWorldSpace(float t) { 211 | Vector3 d = Derivative(t); 212 | if (displayXZ) 213 | d = FlipXYtoXZ(d); 214 | return transform.TransformDirection(d); 215 | } 216 | 217 | /// Get derivative of curve between one point on the curve and the next in local 2D space 218 | /// Rather than interpolating over the entire curve, this simply interpolates 219 | /// between the point with fromIndex and the next segment 220 | /// This is not normalised by default in case you don't need that 221 | public Vector2 Derivative(int fromIndex, float t) { 222 | InitSpline(); 223 | return spline.Derivative(fromIndex, t); 224 | } 225 | 226 | /// Get derivative of curve between one point on the curve and the next in world space 227 | /// Rather than interpolating over the entire curve, this simply interpolates 228 | /// between the point with fromIndex and the next segment 229 | /// This is not normalised by default in case you don't need that 230 | public Vector3 DerivativeWorldSpace(int fromIndex, float t) { 231 | Vector3 d = Derivative(fromIndex, t); 232 | if (displayXZ) 233 | d = FlipXYtoXZ(d); 234 | return transform.TransformDirection(d); 235 | } 236 | 237 | /// Convert a physical distance to a t position on the curve. This is 238 | /// approximate, the accuracy of can be changed via LengthSamplesPerSegment 239 | public float DistanceToLinearT(float dist) { 240 | InitSpline(); 241 | return spline.DistanceToLinearT(dist); 242 | } 243 | 244 | /// Interpolate a position on the entire curve based on distance in local 2D space. This is 245 | /// approximate, the accuracy of can be changed via LengthSamplesPerSegment 246 | public Vector2 InterpolateDistance(float dist) { 247 | InitSpline(); 248 | return spline.InterpolateDistance(dist); 249 | } 250 | 251 | /// Interpolate a position on the entire curve based on distance in world space. This is 252 | /// approximate, the accuracy of can be changed via LengthSamplesPerSegment 253 | public Vector3 InterpolateDistanceWorldSpace(float dist) { 254 | Vector3 p = InterpolateDistance(dist); 255 | if (displayXZ) 256 | p = FlipXYtoXZ(p); 257 | return transform.TransformPoint(p); 258 | } 259 | 260 | /// Get derivative of the curve at a point long the curve at a distance, in local 2D space. This 261 | /// is approximate, the accuracy of this can be changed via 262 | /// LengthSamplesPerSegment 263 | public Vector2 DerivativeDistance(float dist) { 264 | InitSpline(); 265 | return spline.DerivativeDistance(dist); 266 | } 267 | 268 | /// Get derivative of the curve at a point long the curve at a distance in world space. This 269 | /// is approximate, the accuracy of this can be changed via 270 | /// LengthSamplesPerSegment 271 | public Vector2 DerivativeDistanceWorldSpace(float dist) { 272 | Vector3 d = DerivativeDistance(dist); 273 | if (displayXZ) 274 | d = FlipXYtoXZ(d); 275 | return transform.TransformDirection(d); 276 | } 277 | 278 | // Editor functions 279 | void OnDrawGizmos() { 280 | DrawCurveGizmo(); 281 | } 282 | 283 | void OnDrawGizmosSelected() { 284 | DrawNormalsGizmo(); 285 | DrawDistancesGizmo(); 286 | } 287 | 288 | private void DrawCurveGizmo() { 289 | if (Count == 0) { 290 | return; 291 | } 292 | float sampleWidth = 1.0f / ((float)Count * stepsPerSegment); 293 | Gizmos.color = Color.yellow; 294 | Vector3 plast = GetPointWorldSpace(0); 295 | for (float t = sampleWidth; t <= 1.0f; t += sampleWidth) { 296 | Vector3 p = InterpolateWorldSpace(t); 297 | 298 | Gizmos.DrawLine(plast, p); 299 | plast = p; 300 | } 301 | } 302 | 303 | private void DrawNormalsGizmo() { 304 | if (Count == 0 || !showNormals) { 305 | return; 306 | } 307 | float sampleWidth = 1.0f / ((float)Count * stepsPerSegment); 308 | Gizmos.color = Color.magenta; 309 | for (float t = 0.0f; t <= 1.0f; t += sampleWidth) { 310 | Vector3 p = InterpolateWorldSpace(t); 311 | Vector3 tangent = DerivativeWorldSpace(t); 312 | tangent.Normalize(); 313 | tangent *= normalDisplayLength; 314 | p -= tangent * 0.5f; 315 | Gizmos.DrawLine(p, p + tangent); 316 | } 317 | } 318 | 319 | private void DrawDistancesGizmo() { 320 | if (Count == 0 || !showDistance || distanceMarker <= 0.0f) { 321 | return; 322 | } 323 | float len = Length; 324 | Gizmos.color = Color.green; 325 | Quaternion rot90 = Quaternion.AngleAxis(90, transform.TransformDirection(displayXZ ? Vector3.up : Vector3.forward)); 326 | 327 | for (float dist = 0.0f; dist <= len; dist += distanceMarker) { 328 | // Just so we only have to perform the dist->t calculation once 329 | // for both position & tangent 330 | float t = DistanceToLinearT(dist); 331 | Vector3 p = InterpolateWorldSpace(t); 332 | Vector3 tangent = DerivativeWorldSpace(t); 333 | // Rotate tangent 90 degrees so we can render marker 334 | tangent.Normalize(); 335 | tangent = rot90 * tangent; 336 | Vector3 t1 = p + tangent; 337 | Vector3 t2 = p - tangent; 338 | Gizmos.DrawLine(t1, t2); 339 | } 340 | 341 | } 342 | 343 | private static Vector3 FlipXYtoXZ(Vector3 inp) { 344 | return new Vector3(inp.x, 0, inp.y); 345 | } 346 | private static Vector3 FlipXZtoXY(Vector3 inp) { 347 | return new Vector3(inp.x, inp.z, 0); 348 | } 349 | 350 | } -------------------------------------------------------------------------------- /Spline2DComponent.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 25474e02d87cf5a46b5931c38dc7cb0e 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sinbad/UnitySpline2D/26be60c04ea3075d9548a871a5a48a71d8a6d0ae/screenshot.png -------------------------------------------------------------------------------- /screenshot.png.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f67ea6a8296ed6a4aba59d3f594e4dca 3 | TextureImporter: 4 | fileIDToRecycleName: {} 5 | externalObjects: {} 6 | serializedVersion: 9 7 | mipmaps: 8 | mipMapMode: 0 9 | enableMipMap: 1 10 | sRGBTexture: 1 11 | linearTexture: 0 12 | fadeOut: 0 13 | borderMipMap: 0 14 | mipMapsPreserveCoverage: 0 15 | alphaTestReferenceValue: 0.5 16 | mipMapFadeDistanceStart: 1 17 | mipMapFadeDistanceEnd: 3 18 | bumpmap: 19 | convertToNormalMap: 0 20 | externalNormalMap: 0 21 | heightScale: 0.25 22 | normalMapFilter: 0 23 | isReadable: 0 24 | streamingMipmaps: 0 25 | streamingMipmapsPriority: 0 26 | grayScaleToAlpha: 0 27 | generateCubemap: 6 28 | cubemapConvolution: 0 29 | seamlessCubemap: 0 30 | textureFormat: 1 31 | maxTextureSize: 2048 32 | textureSettings: 33 | serializedVersion: 2 34 | filterMode: -1 35 | aniso: -1 36 | mipBias: -100 37 | wrapU: -1 38 | wrapV: -1 39 | wrapW: -1 40 | nPOTScale: 1 41 | lightmap: 0 42 | compressionQuality: 50 43 | spriteMode: 0 44 | spriteExtrude: 1 45 | spriteMeshType: 1 46 | alignment: 0 47 | spritePivot: {x: 0.5, y: 0.5} 48 | spritePixelsToUnits: 100 49 | spriteBorder: {x: 0, y: 0, z: 0, w: 0} 50 | spriteGenerateFallbackPhysicsShape: 1 51 | alphaUsage: 1 52 | alphaIsTransparency: 0 53 | spriteTessellationDetail: -1 54 | textureType: 0 55 | textureShape: 1 56 | singleChannelComponent: 0 57 | maxTextureSizeSet: 0 58 | compressionQualitySet: 0 59 | textureFormatSet: 0 60 | platformSettings: 61 | - serializedVersion: 2 62 | buildTarget: DefaultTexturePlatform 63 | maxTextureSize: 2048 64 | resizeAlgorithm: 0 65 | textureFormat: -1 66 | textureCompression: 1 67 | compressionQuality: 50 68 | crunchedCompression: 0 69 | allowsAlphaSplitting: 0 70 | overridden: 0 71 | androidETC2FallbackOverride: 0 72 | spriteSheet: 73 | serializedVersion: 2 74 | sprites: [] 75 | outline: [] 76 | physicsShape: [] 77 | bones: [] 78 | spriteID: 79 | vertices: [] 80 | indices: 81 | edges: [] 82 | weights: [] 83 | spritePackingTag: 84 | pSDRemoveMatte: 0 85 | pSDShowRemoveMatteOption: 0 86 | userData: 87 | assetBundleName: 88 | assetBundleVariant: 89 | --------------------------------------------------------------------------------