├── 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 | 
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 |
--------------------------------------------------------------------------------