Skip to content

Commit 6f93cf2

Browse files
committed
- You can now store intermediate normal vectors along the spline. This vastly improves the accuracy of the normals. This doesn't affect the normals auto calculation performance because intermediate normals were calculated anyways, they just weren't stored
- Added option to view intermediate normal vectors in Scene view and not just the end points' normals. This helps visualize the normals along the spline. Note that, despite what the name suggests, this function doesn't require intermediate normals to be calculated, it simply uses BezierSpline's GetNormal function and renders those normals in Scene view
1 parent 2bf12b5 commit 6f93cf2

13 files changed

+260
-65
lines changed

.github/README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ spline.Initialize( 2 );
7575

7676
You can change the position, rotation, scale and normal values of the end points, as well as the positions of their control points to reshape the spline.
7777

78-
End points have the following properties to store their transformational data: `position`, `localPosition`, `rotation`, `localRotation`, `eulerAngles`, `localEulerAngles`, `localScale`, `normal` and `autoCalculatedNormalAngleOffset`.
78+
End points have the following properties to store their transformational data: `position`, `localPosition`, `rotation`, `localRotation`, `eulerAngles`, `localEulerAngles`, `localScale`, `normal`, `autoCalculatedNormalAngleOffset` and `intermediateNormals`.
7979

8080
Positions of control points can be tweaked using the following properties in BezierPoint: `precedingControlPointPosition`, `precedingControlPointLocalPosition`, `followingControlPointPosition` and `followingControlPointLocalPosition`. The local positions are relative to their corresponding end points.
8181

@@ -110,9 +110,9 @@ If you want to create a linear path between the end points of the spline, you ca
110110

111111
- **Auto calculate the normals**
112112

113-
If you want to calculate the spline's normal vectors automatically, you can call the **AutoCalculateNormals( float normalAngle = 0f, int smoothness = 10 )** function (or, to call this function automatically when spline's end points are modified, simply change the spline's **autoCalculateNormals** and **autoCalculatedNormalsAngle** properties). All resulting normal vectors will be rotated around their Z axis by "normalAngle" degrees. Additionally, each end point's normal vector will be rotated by that end point's "autoCalculatedNormalAngleOffset" degrees. "smoothness" determines how many intermediate steps are taken between each consecutive end point to calculate those end points' normal vectors. More intermediate steps is better but also slower to calculate.
113+
If you want to calculate the spline's normal vectors automatically, you can call the **AutoCalculateNormals( float normalAngle = 0f, int smoothness = 10, bool calculateIntermediateNormals = false )** function (or, to call this function automatically when spline's end points are modified, simply change the spline's **autoCalculateNormals**, **autoCalculatedNormalsAngle** and **m_autoCalculatedIntermediateNormalsCount** properties). All resulting normal vectors will be rotated around their Z axis by "normalAngle" degrees. Additionally, each end point's normal vector will be rotated by that end point's "autoCalculatedNormalAngleOffset" degrees. "smoothness" determines how many intermediate steps are taken between each consecutive end point to calculate those end points' normal vectors. More intermediate steps is better but also slower to calculate. When "calculateIntermediateNormals" is enabled, calculated intermediate normals (determined by "smoothness") are cached at each end point. This results in smoother linear interpolation for normals. Otherwise, the intermediate normals aren't stored anywhere and only the end points' normals are used to estimate normals along the spline.
114114

115-
If auto calculated normals don't look quite right despite modifying the "normalAngle" (*Auto Calculated Normals Angle* in the Inspector) and "autoCalculatedNormalAngleOffset" (*Normal Angle* in the Inspector) variables, you can either consider inserting new end points to the sections of the spline that normals don't behave correctly, or setting the normals manually.
115+
If auto calculated normals don't look quite right despite modifying the "calculateIntermediateNormals" (*Auto Calculated Intermediate Normals* in the Inspector), "normalAngle" (*Auto Calculated Normals Angle* in the Inspector) and "autoCalculatedNormalAngleOffset" (*Normal Angle* in the Inspector) variables, you can either consider inserting new end points to the sections of the spline that normals don't behave correctly, or setting the normals manually.
116116

117117
- **Get notified when spline is modified**
118118

@@ -136,7 +136,7 @@ Tangent is calculated using the first derivative of the spline formula and gives
136136

137137
- `Vector3 GetNormal( float normalizedT )`
138138

139-
Interpolates between the end points' normal vectors. Note that this plugin doesn't store any intermediate data between end point pairs, so if two consecutive end points have almost the opposite tangents, then their interpolated normal vector may not be correct at some parts of the spline. Inserting a new end point between these two end points could resolve this issue. By default, all normal vectors have value (0,1,0).
139+
Interpolates between the end points' normal vectors. If intermediate normals are calculated, they are interpolated to calculate the result. Otherwise, only the end points' normal vectors are interpolated and the resulting normal vectors may not be correct at some parts of the spline. Inserting new end point(s) to those sections of the spline could resolve this issue. By default, all normal vectors have value (0,1,0).
140140

141141
- `BezierPoint.ExtraData GetExtraData( float normalizedT )`
142142

Plugins/BezierSolution/BezierPoint.cs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace BezierSolution
44
{
55
[AddComponentMenu( "Bezier Solution/Bezier Point" )]
6+
[HelpURL( "https://github.com/yasirkula/UnityBezierSolution" )]
67
public partial class BezierPoint : MonoBehaviour
78
{
89
public Vector3 localPosition
@@ -289,6 +290,23 @@ public float autoCalculatedNormalAngleOffset
289290
}
290291
}
291292

293+
[SerializeField, HideInInspector]
294+
private Vector3[] m_intermediateNormals;
295+
public Vector3[] intermediateNormals
296+
{
297+
get { return m_intermediateNormals; }
298+
set
299+
{
300+
// In this special case, don't early exit if the assigned array is the same because one of its elements might have changed.
301+
// We can safely early exit if the assigned value was null or empty, though
302+
if( ( m_intermediateNormals == null || m_intermediateNormals.Length == 0 ) && ( value == null || value.Length == 0 ) )
303+
return;
304+
305+
m_intermediateNormals = value;
306+
spline.dirtyFlags |= InternalDirtyFlags.NormalChange;
307+
}
308+
}
309+
292310
[SerializeField, HideInInspector]
293311
[UnityEngine.Serialization.FormerlySerializedAs( "extraData" )]
294312
private ExtraData m_extraData;
@@ -387,6 +405,31 @@ internal void RefreshIfChanged()
387405
}
388406
}
389407

408+
internal void SetNormalAndResetIntermediateNormals( Vector3 normal, string undo )
409+
{
410+
if( spline && spline.autoCalculateNormals )
411+
return;
412+
413+
#if UNITY_EDITOR
414+
if( !string.IsNullOrEmpty( undo ) )
415+
UnityEditor.Undo.RecordObject( this, undo );
416+
#endif
417+
418+
this.normal = normal;
419+
intermediateNormals = null;
420+
421+
BezierPoint previousPoint = this.previousPoint;
422+
if( previousPoint && previousPoint.m_intermediateNormals != null && previousPoint.m_intermediateNormals.Length > 0 )
423+
{
424+
#if UNITY_EDITOR
425+
if( !string.IsNullOrEmpty( undo ) )
426+
UnityEditor.Undo.RecordObject( previousPoint, undo );
427+
#endif
428+
429+
previousPoint.intermediateNormals = null;
430+
}
431+
}
432+
390433
public void Reset()
391434
{
392435
localPosition = Vector3.zero;

Plugins/BezierSolution/BezierSpline.cs

Lines changed: 107 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
namespace BezierSolution
1212
{
1313
[AddComponentMenu( "Bezier Solution/Bezier Spline" )]
14+
[HelpURL( "https://github.com/yasirkula/UnityBezierSolution" )]
1415
[ExecuteInEditMode]
1516
public partial class BezierSpline : MonoBehaviour, IEnumerable<BezierPoint>
1617
{
@@ -107,6 +108,23 @@ public float autoCalculatedNormalsAngle
107108
}
108109
}
109110

111+
[SerializeField, HideInInspector]
112+
private int m_autoCalculatedIntermediateNormalsCount = 10;
113+
public int autoCalculatedIntermediateNormalsCount
114+
{
115+
get { return m_autoCalculatedIntermediateNormalsCount; }
116+
set
117+
{
118+
value = Mathf.Clamp( value, 0, 999 );
119+
120+
if( m_autoCalculatedIntermediateNormalsCount != value )
121+
{
122+
m_autoCalculatedIntermediateNormalsCount = value;
123+
dirtyFlags |= InternalDirtyFlags.NormalOffsetChange;
124+
}
125+
}
126+
}
127+
110128
private EvenlySpacedPointsHolder m_evenlySpacedPoints = null;
111129
public EvenlySpacedPointsHolder evenlySpacedPoints
112130
{
@@ -199,9 +217,12 @@ internal void CheckDirty()
199217
}
200218
else
201219
{
202-
AutoCalculateNormals( m_autoCalculatedNormalsAngle );
220+
if( m_autoCalculatedIntermediateNormalsCount <= 0 )
221+
AutoCalculateNormals( m_autoCalculatedNormalsAngle, calculateIntermediateNormals: false );
222+
else
223+
AutoCalculateNormals( m_autoCalculatedNormalsAngle, m_autoCalculatedIntermediateNormalsCount + 1, true );
203224

204-
// If an end point's normal vector was changed only, we've reverted that change by auto calculating the normals again
225+
// If an end point's only normal vector was changed, we've reverted that change by auto calculating the normals again
205226
dirtyFlags &= ~InternalDirtyFlags.NormalChange;
206227

207228
// If an end point's position or normal calculation offset was changed, then the spline's normals have indeed changed
@@ -636,11 +657,20 @@ public Vector3 GetNormal( float normalizedT )
636657
if( endIndex == endPoints.Count )
637658
endIndex = 0;
638659

660+
float localT = t - startIndex;
661+
662+
Vector3[] intermediateNormals = endPoints[startIndex].intermediateNormals;
663+
if( intermediateNormals != null && intermediateNormals.Length > 0 )
664+
{
665+
localT *= intermediateNormals.Length - 1;
666+
int localStartIndex = (int) localT;
667+
668+
return ( localStartIndex < intermediateNormals.Length - 1 ) ? Vector3.LerpUnclamped( intermediateNormals[localStartIndex], intermediateNormals[localStartIndex + 1], localT - localStartIndex ) : intermediateNormals[localStartIndex];
669+
}
670+
639671
Vector3 startNormal = endPoints[startIndex].normal;
640672
Vector3 endNormal = endPoints[endIndex].normal;
641673

642-
float localT = t - startIndex;
643-
644674
Vector3 normal = Vector3.LerpUnclamped( startNormal, endNormal, localT );
645675
if( normal.y == 0f && normal.x == 0f && normal.z == 0f )
646676
{
@@ -1091,14 +1121,18 @@ public void AutoConstructSpline2()
10911121

10921122
// Credit: https://stackoverflow.com/a/14241741/2373034
10931123
// Alternative approach: https://stackoverflow.com/a/25458216/2373034
1094-
public void AutoCalculateNormals( float normalAngle = 0f, int smoothness = 10 )
1124+
public void AutoCalculateNormals( float normalAngle = 0f, int smoothness = 10, bool calculateIntermediateNormals = false )
10951125
{
10961126
for( int i = 0; i < endPoints.Count; i++ )
10971127
endPoints[i].RefreshIfChanged();
10981128

1129+
Vector3 tangent = new Vector3(), rotatedNormal = new Vector3();
10991130
smoothness = Mathf.Max( 1, smoothness );
11001131
float _1OverSmoothness = 1f / smoothness;
11011132

1133+
if( smoothness <= 1 )
1134+
calculateIntermediateNormals = false;
1135+
11021136
// Calculate initial point's normal using Frenet formula
11031137
Segment segment = new Segment( endPoints[0], endPoints[1], 0f );
11041138
Vector3 tangent1 = segment.GetTangent( 0f ).normalized;
@@ -1107,7 +1141,9 @@ public void AutoCalculateNormals( float normalAngle = 0f, int smoothness = 10 )
11071141
if( Mathf.Approximately( cross.sqrMagnitude, 0f ) ) // This is not a curved spline but rather a straight line
11081142
cross = Vector3.Cross( tangent2, ( tangent2.x != 0f || tangent2.z != 0f ) ? Vector3.up : Vector3.forward );
11091143

1110-
endPoints[0].normal = Vector3.Cross( cross, tangent1 ).normalized;
1144+
// prevNormal stores the unrotated normal whereas endpoints[index].normal stores the rotated normal
1145+
Vector3 prevNormal = Vector3.Cross( cross, tangent1 ).normalized;
1146+
endPoints[0].normal = Quaternion.AngleAxis( normalAngle + endPoints[0].autoCalculatedNormalAngleOffset, tangent1 ) * prevNormal;
11111147

11121148
// Calculate other points' normals by iteratively (smoothness) calculating normals between the previous point and the next point
11131149
for( int i = 0; i < endPoints.Count; i++ )
@@ -1119,35 +1155,73 @@ public void AutoCalculateNormals( float normalAngle = 0f, int smoothness = 10 )
11191155
else
11201156
break;
11211157

1122-
Vector3 prevNormal = endPoints[i].normal;
1158+
Vector3[] intermediateNormals = null;
1159+
if( !calculateIntermediateNormals )
1160+
segment.point1.intermediateNormals = null;
1161+
else
1162+
{
1163+
intermediateNormals = segment.point1.intermediateNormals;
1164+
if( intermediateNormals == null || intermediateNormals.Length != smoothness + 1 )
1165+
segment.point1.intermediateNormals = intermediateNormals = new Vector3[smoothness + 1];
1166+
1167+
intermediateNormals[0] = segment.point1.normal;
1168+
}
1169+
1170+
float normalAngle1 = normalAngle + segment.point1.autoCalculatedNormalAngleOffset;
1171+
float normalAngle2 = normalAngle + segment.point2.autoCalculatedNormalAngleOffset;
1172+
11231173
for( int j = 1; j <= smoothness; j++ )
11241174
{
1125-
Vector3 tangent = segment.GetTangent( j * _1OverSmoothness ).normalized;
1175+
float localT = j * _1OverSmoothness;
1176+
tangent = segment.GetTangent( localT ).normalized;
11261177
prevNormal = Vector3.Cross( tangent, Vector3.Cross( prevNormal, tangent ).normalized ).normalized;
1178+
1179+
if( calculateIntermediateNormals )
1180+
{
1181+
float _normalAngle = Mathf.LerpUnclamped( normalAngle1, normalAngle2, localT );
1182+
intermediateNormals[j] = rotatedNormal = ( _normalAngle == 0f ) ? prevNormal : ( Quaternion.AngleAxis( _normalAngle, tangent ) * prevNormal );
1183+
}
11271184
}
11281185

1129-
if( i < endPoints.Count - 1 )
1130-
endPoints[i + 1].normal = prevNormal;
1131-
else if( prevNormal != -endPoints[0].normal )
1132-
endPoints[0].normal = ( endPoints[0].normal + prevNormal ).normalized;
1133-
}
1186+
if( !calculateIntermediateNormals )
1187+
rotatedNormal = ( normalAngle2 == 0f ) ? prevNormal : ( Quaternion.AngleAxis( normalAngle2, tangent ) * prevNormal );
11341188

1135-
// Rotate normals
1136-
for( int i = 0; i < endPoints.Count; i++ )
1137-
{
1138-
float rotateAngle = normalAngle + endPoints[i].autoCalculatedNormalAngleOffset;
1139-
if( Mathf.Approximately( rotateAngle, 180f ) )
1140-
endPoints[i].normal = -endPoints[i].normal;
1141-
else if( !Mathf.Approximately( rotateAngle, 0f ) )
1189+
if( i < endPoints.Count - 1 )
1190+
endPoints[i + 1].normal = rotatedNormal;
1191+
else
11421192
{
1143-
if( i < endPoints.Count - 1 )
1144-
segment = new Segment( endPoints[i], endPoints[i + 1], 0f );
1145-
else if( m_loop )
1146-
segment = new Segment( endPoints[i], endPoints[0], 0f );
1193+
if( !calculateIntermediateNormals )
1194+
{
1195+
if( rotatedNormal != -endPoints[0].normal )
1196+
endPoints[0].normal = ( endPoints[0].normal + rotatedNormal ).normalized;
1197+
}
11471198
else
1148-
segment = new Segment( endPoints[i - 1], endPoints[i], 1f );
1149-
1150-
endPoints[i].normal = Quaternion.AngleAxis( rotateAngle, segment.GetTangent() ) * endPoints[i].normal;
1199+
{
1200+
// In a looping spline, the first end point's normal value is a special case because the initial value that we've assigned to it
1201+
// might end up vastly different from the final rotatedNormal that we've found. To accommodate to this change, we'll find the
1202+
// angle difference between these two values and gradually apply that difference to the first end point's intermediate normals
1203+
Vector3 initialNormal0 = endPoints[0].normal;
1204+
float normal0DeltaAngle = Vector3.Angle( initialNormal0, rotatedNormal );
1205+
if( Mathf.Abs( normal0DeltaAngle ) > 5f )
1206+
{
1207+
// Vector3.SignedAngle: https://github.com/Unity-Technologies/UnityCsReference/blob/61f92bd79ae862c4465d35270f9d1d57befd1761/Runtime/Export/Math/Vector3.cs#L316-L328
1208+
// The function itself isn't available on Unity 5.6 so its source code is copy&pasted here
1209+
float cross_x = initialNormal0.y * rotatedNormal.z - initialNormal0.z * rotatedNormal.y;
1210+
float cross_y = initialNormal0.z * rotatedNormal.x - initialNormal0.x * rotatedNormal.z;
1211+
float cross_z = initialNormal0.x * rotatedNormal.y - initialNormal0.y * rotatedNormal.x;
1212+
normal0DeltaAngle *= Mathf.Sign( tangent.x * cross_x + tangent.y * cross_y + tangent.z * cross_z );
1213+
1214+
segment = new Segment( endPoints[0], endPoints[1], 0f );
1215+
intermediateNormals = endPoints[0].intermediateNormals;
1216+
endPoints[0].normal = intermediateNormals[0] = rotatedNormal;
1217+
1218+
for( int j = 1; j < smoothness; j++ )
1219+
{
1220+
float localT = j * _1OverSmoothness;
1221+
intermediateNormals[j] = Quaternion.AngleAxis( Mathf.LerpUnclamped( normal0DeltaAngle, 0f, localT ), segment.GetTangent( localT ).normalized ) * intermediateNormals[j];
1222+
}
1223+
}
1224+
}
11511225
}
11521226
}
11531227
}
@@ -1285,6 +1359,12 @@ public PointCache GeneratePointCache( EvenlySpacedPointsHolder lookupTable, Extr
12851359
return new PointCache( positions, normals, tangents, bitangents, extraDatas, loop );
12861360
}
12871361

1362+
public void ClearIntermediateNormals()
1363+
{
1364+
for( int i = 0; i < endPoints.Count; i++ )
1365+
endPoints[i].intermediateNormals = null;
1366+
}
1367+
12881368
private float AccuracyToStepSize( float accuracy )
12891369
{
12901370
if( accuracy <= 0f )

Plugins/BezierSolution/Editor/BezierPointEditor.cs

Lines changed: 6 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -847,10 +847,7 @@ public override void OnInspectorGUI()
847847
if( EditorGUI.EndChangeCheck() )
848848
{
849849
for( int i = 0; i < allPoints.Length; i++ )
850-
{
851-
Undo.RecordObject( allPoints[i], "Change Normal" );
852-
allPoints[i].normal = normal;
853-
}
850+
allPoints[i].SetNormalAndResetIntermediateNormals( normal, "Change Normal" );
854851

855852
SceneView.RepaintAll();
856853
}
@@ -860,10 +857,7 @@ public override void OnInspectorGUI()
860857
{
861858
Vector3 cameraPos = SceneView.lastActiveSceneView.camera.transform.position;
862859
for( int i = 0; i < allPoints.Length; i++ )
863-
{
864-
Undo.RecordObject( allPoints[i], "Change Normal" );
865-
allPoints[i].normal = ( cameraPos - allPoints[i].position ).normalized;
866-
}
860+
allPoints[i].SetNormalAndResetIntermediateNormals( ( cameraPos - allPoints[i].position ).normalized, "Change Normal" );
867861

868862
SceneView.RepaintAll();
869863
}
@@ -916,8 +910,7 @@ public override void OnInspectorGUI()
916910
else
917911
tangent = new BezierSpline.Segment( spline[index - 1], spline[index], 1f ).GetTangent();
918912

919-
Undo.RecordObject( allPoints[i], "Change Normal Rotate Angle" );
920-
allPoints[i].normal = Quaternion.AngleAxis( normalRotationAngle, tangent ) * allPoints[i].normal;
913+
allPoints[i].SetNormalAndResetIntermediateNormals( Quaternion.AngleAxis( normalRotationAngle, tangent ) * allPoints[i].normal, "Change Normal Rotate Angle" );
921914
}
922915
}
923916

@@ -1260,31 +1253,22 @@ internal static void CalculateInsertedPointPosition( BezierPoint neighbor1, Bezi
12601253
private void FlipNormals( IList<BezierPoint> points )
12611254
{
12621255
for( int i = 0; i < points.Count; i++ )
1263-
{
1264-
Undo.RecordObject( points[i], "Flip Normals" );
1265-
points[i].normal = -points[i].normal;
1266-
}
1256+
points[i].SetNormalAndResetIntermediateNormals( -points[i].normal, "Flip Normals" );
12671257
}
12681258

12691259
private void NormalizeNormals( IList<BezierPoint> points )
12701260
{
12711261
for( int i = 0; i < points.Count; i++ )
12721262
{
12731263
if( points[i].normal != Vector3.zero )
1274-
{
1275-
Undo.RecordObject( points[i], "Normalize Normals" );
1276-
points[i].normal = points[i].normal.normalized;
1277-
}
1264+
points[i].SetNormalAndResetIntermediateNormals( points[i].normal.normalized, "Normalize Normals" );
12781265
}
12791266
}
12801267

12811268
private void ResetNormals( IList<BezierPoint> points )
12821269
{
12831270
for( int i = 0; i < points.Count; i++ )
1284-
{
1285-
Undo.RecordObject( points[i], "Reset Normals" );
1286-
points[i].normal = Vector3.up;
1287-
}
1271+
points[i].SetNormalAndResetIntermediateNormals( Vector3.up, "Reset Normals" );
12881272
}
12891273

12901274
private void OnUndoRedo()

0 commit comments

Comments
 (0)