Skip to content

Commit ca6912d

Browse files
committed
- Normals auto calculation algorithm now generates smoother normals
- Updated documentation
1 parent 35b5a90 commit ca6912d

File tree

5 files changed

+105
-61
lines changed

5 files changed

+105
-61
lines changed

.github/README.md

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ There are 5 ways to install this plugin:
2828

2929
## CREATING & EDITING A NEW SPLINE IN EDITOR
3030

31-
To create a new spline in the editor, follow "GameObject - Bezier Spline".
31+
To create a new spline in the editor, click **GameObject - Bezier Spline**.
3232

3333
Now you can select the end points of the spline in the Scene view and translate/rotate/scale or delete/duplicate them as you wish (each end point has 2 control points, which can also be translated):
3434

@@ -42,10 +42,20 @@ The user interface for the spline editor should be pretty self-explanatory. Howe
4242

4343
**Draw Runtime Gizmos:** draws the spline during gameplay
4444

45+
**Show Control Points:** sets whether or not the control points of the end points will be drawn in Scene window
46+
47+
**Show Normals:** sets whether or not the end points' normals will be drawn in Scene window
48+
49+
**Auto Calculated Normals Angle:** when *Auto Calculate Normals* button is clicked, all normals will be rotated around their Z axis by the specified amount (each end point's rotation angle can further be customized from the end point's Inspector)
50+
4551
**Construct Linear Path:** constructs a completely linear path between the end points by using *Free* handle mode and adjusting the control points of end points (see *Convert spline to a linear path* section below). Enabling the **Always** option will apply this technique to the spline whenever its end points change (Editor-only)
4652

4753
**Auto Construct Spline:** auto adjusts the control points of end points to form a smooth spline that goes through the end points you set. There are 2 different implementations for it, with each giving a slightly different output (see *Auto construct the spline* section below)
4854

55+
**Auto Calculate Normals:** attempts to automatically calculate the end points' normal vectors
56+
57+
**Insert Point At Cursor:** quickly inserts new end points to the clicked sections of the spline
58+
4959
**Handle Mode:** control points of end points are handled in one of 3 ways: Free mode allows moving control points independently, Mirrored mode places the control points opposite to each other and Aligned mode ensures that both control points are aligned on a line that passes through the end point (unlike Mirrored mode, their distance to end point may differ)
5060

5161
**Extra Data:** end points can store additional data that can hold 4 floats. You can interpolate between points' extra data by code (see *UTILITY FUNCTIONS* section below). This extra data is especially useful for moving a camera on a bezier spline while setting different camera rotations at each end point (the BezierWalker components can read that data). You can click the **C** button to store the Scene camera's current rotation in this extra data. Then, you can visualize this data by clicking the **V** button
@@ -71,15 +81,15 @@ spline.Initialize( 2 );
7181

7282
`void SwapPointsAt( int index1, int index2 )`: swaps indices of two end points
7383

74-
`void MovePoint( int previousIndex, int newIndex )`: changes an end point's index
84+
`void ChangePointIndex( int previousIndex, int newIndex )`: changes an end point's index
7585

7686
`int IndexOf( BezierPoint point )`: returns the index of an end point
7787

7888
- **Shape the spline**
7989

80-
You can change the position, rotation and scale values of end points and the positions of their control points to reshape the spline.
90+
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.
8191

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

8494
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.
8595

@@ -100,7 +110,7 @@ spline[0].followingControlPointPosition = spline[1].position;
100110

101111
- **Auto construct the spline**
102112

103-
If you don't want to position all the control points manually, but rather generate a nice-looking "continuous" spline that goes through the end points you have created, you can call either **AutoConstructSpline()** or **AutoConstructSpline2()**. These methods are implementations of some algorithms found on the internet (and credited in the source code). There is a third algorithm (*AutoConstructSpline3()*) which is not implemented, but feel free to implement it yourself!
113+
If you don't want to position all the control points manually, but rather generate a nice-looking "continuous" spline that goes through the end points you have created, you can call either **AutoConstructSpline()** or **AutoConstructSpline2()**. These methods are implementations of some algorithms found on the internet (and credited in the source code).
104114

105115
![auto-construct](Images/4.png)
106116

@@ -110,6 +120,12 @@ If you want to create a linear path between the end points of the spline, you ca
110120

111121
![auto-construct](Images/4_2.png)
112122

123+
- **Auto calculate the normals**
124+
125+
If you want to calculate the spline's normal vectors automatically, you can call the **AutoCalculateNormals( float normalAngle = 0f, int smoothness = 10 )** function. 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.
126+
127+
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.
128+
113129
## UTILITY FUNCTIONS
114130

115131
The framework comes with some utility functions. These functions are not necessarily perfect but most of the time, they get the job done. Though, if you want, you can use this framework to just create splines and then apply your own logic to them.
@@ -122,6 +138,10 @@ A spline is essentially a mathematical formula with a \[0,1\] clamped input (usu
122138

123139
Tangent is calculated using the first derivative of the spline formula and gives the direction of the movement at a given point on the spline. Can be used to determine which direction an object on the spline should look at at a given point.
124140

141+
- `Vector3 GetNormal( float normalizedT )`
142+
143+
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).
144+
125145
- `BezierPoint.ExtraData GetExtraData( float normalizedT )`
126146

127147
Interpolates between the extra data provided at each end point. This data has 4 float components and can implicitly be converted to Vector2, Vector3, Vector4, Quaternion, Rect, Vector2Int, Vector3Int and RectInt.
@@ -138,25 +158,29 @@ Calculates the approximate length of a segment of the spline. To calculate the l
138158

139159
- `PointIndexTuple GetNearestPointIndicesTo( float normalizedT )`
140160

141-
Returns the indices of the two end points that are closest to *normalizedT*. The *PointIndexTuple* struct also holds a *t* value in range \[0,1\], which can be used to interpolate between the properties of the two end points at these indices.
161+
Returns the indices of the two end points that are closest to *normalizedT*. The *PointIndexTuple* struct also holds a *localT* value in range \[0,1\], which can be used to interpolate between the properties of the two end points at these indices. You can also call the `GetPoint()`, `GetTangent()`, `GetNormal()` and `GetExtraData()` functions of this struct and the returned values will be calculated as if the spline consisted of only these two end points.
142162

143163
- `Vector3 FindNearestPointTo( Vector3 worldPos, out float normalizedT, float accuracy = 100f )`
144164

145165
Finds the nearest point on the spline to any given point in 3D space. The normalizedT parameter is optional and it returns the parameter *t* corresponding to the resulting point. To find the nearest point, the spline is divided into "accuracy" points and the nearest point is selected. Thus, the result will not be 100% accurate but will be good enough for casual use-cases.
146166

167+
- `Vector3 FindNearestPointToLine( Vector3 lineStart, Vector3 lineEnd, out Vector3 pointOnLine, out float normalizedT, float accuracy = 100f )`
168+
169+
Finds the nearest point on the spline to the given line in 3D space. The pointOnLine and normalizedT parameters are optional.
170+
147171
- `Vector3 MoveAlongSpline( ref float normalizedT, float deltaMovement, int accuracy = 3 )`
148172

149173
Moves a point (normalizedT) on the spline deltaMovement units ahead and returns the resulting point. The normalizedT parameter is passed by reference to keep track of the new *t* parameter.
150174

151175
## OTHER COMPONENTS
152176

153-
Framework comes with 3 additional components that may help you move objects or particles along splines. These components are located in the Utilities folder.
177+
Framework comes with 4 additional components that may help you move objects or particles along splines. These components are located in the Utilities folder.
154178

155179
- **BezierWalkerWithSpeed**
156180

157181
![walker-with-speed](Images/5.png)
158182

159-
Moves an object along a spline with constant speed. There are 3 travel modes: Once, Ping Pong and Loop. If *Look At* is Forward, the object will always face forwards. If it is SplineExtraData, the extra data stored in the spline's end points is used to determine the rotation. You can modify this extra data from the points' Inspector. The smoothness of the rotation can be adjusted via *Rotation Lerp Modifier*. *Normalized T* determines the starting point. Each time the object completes a lap, its *On Path Completed ()* event is invoked. To see this component in action without entering Play mode, click the *Simulate In Editor* button.
183+
Moves an object along a spline with constant speed. There are 3 travel modes: Once, Ping Pong and Loop. If *Look At* is Forward, the object will always face forwards (end points' normal vectors will be used as up vectors). If it is SplineExtraData, the extra data stored in the spline's end points is used to determine the rotation. You can modify this extra data from the points' Inspector. The smoothness of the rotation can be adjusted via *Rotation Lerp Modifier*. *Normalized T* determines the starting point. Each time the object completes a lap, its *On Path Completed ()* event is invoked. To see this component in action without entering Play mode, click the *Simulate In Editor* button.
160184

161185
- **BezierWalkerWithTime**
162186

Plugins/BezierSolution/BezierSpline.cs

Lines changed: 48 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ public int gizmoSmoothness
154154

155155
[SerializeField]
156156
[HideInInspector]
157-
internal bool Internal_FlipAutoCalculatedNormals = false;
157+
internal float Internal_AutoCalculatedNormalsAngle = 0f;
158158
#endif
159159

160160
public int Count { get { return endPoints.Count; } }
@@ -195,7 +195,7 @@ internal void Internal_CheckDirty()
195195
}
196196

197197
if( Internal_AutoCalculateNormals )
198-
AutoCalculateNormals( Internal_FlipAutoCalculatedNormals );
198+
AutoCalculateNormals( Internal_AutoCalculatedNormalsAngle );
199199

200200
Internal_IsDirty = false;
201201
}
@@ -887,56 +887,69 @@ public void AutoConstructSpline2()
887887
}
888888
}
889889

890-
// Alternative approach1: https://stackoverflow.com/a/25458216/2373034
891-
// Alternative approach2: https://stackoverflow.com/a/14241741/2373034
892-
public void AutoCalculateNormals( bool flipNormals )
890+
// Credit: https://stackoverflow.com/a/14241741/2373034
891+
// Alternative approach: https://stackoverflow.com/a/25458216/2373034
892+
public void AutoCalculateNormals( float normalAngle = 0f, int smoothness = 10 )
893893
{
894-
const float DELTA_T = 0.025f;
895-
const float ONE_MINUS_DELTA_T = 1f - DELTA_T;
896-
897894
for( int i = 0; i < endPoints.Count; i++ )
898895
endPoints[i].RefreshIfChanged();
899896

897+
smoothness = Mathf.Max( 1, smoothness );
898+
float _1OverSmoothness = 1f / smoothness;
899+
900+
// Calculate initial point's normal using Frenet formula
901+
PointIndexTuple tuple = new PointIndexTuple( this, 0, 1, 0f );
902+
Vector3 tangent1 = tuple.GetTangent( 0f ).normalized;
903+
Vector3 tangent2 = tuple.GetTangent( 0.025f ).normalized;
904+
Vector3 cross = Vector3.Cross( tangent2, tangent1 ).normalized;
905+
if( Mathf.Approximately( cross.sqrMagnitude, 0f ) ) // This is not a curved spline but rather a straight line
906+
cross = Vector3.Cross( tangent2, ( tangent2.x != 0f || tangent2.z != 0f ) ? Vector3.up : Vector3.forward );
907+
908+
endPoints[0].normal = Vector3.Cross( cross, tangent1 ).normalized;
909+
910+
// Calculate other points' normals by iteratively (smoothness) calculating normals between the previous point and the next point
900911
for( int i = 0; i < endPoints.Count; i++ )
901912
{
902913
if( i < endPoints.Count - 1 )
903-
endPoints[i].normal = CalculateFrenetNormal( i, i + 1, 0f, DELTA_T, endPoints[i].autoCalculatedNormalAngleOffset );
914+
tuple = new PointIndexTuple( this, i, i + 1, 0f );
904915
else if( loop )
905-
endPoints[i].normal = CalculateFrenetNormal( i, 0, 0f, DELTA_T, endPoints[i].autoCalculatedNormalAngleOffset );
916+
tuple = new PointIndexTuple( this, i, 0, 0f );
906917
else
907-
endPoints[i].normal = CalculateFrenetNormal( i - 1, i, ONE_MINUS_DELTA_T, 1f, endPoints[i].autoCalculatedNormalAngleOffset );
918+
break;
908919

909-
if( flipNormals )
910-
endPoints[i].normal = -endPoints[i].normal;
920+
Vector3 prevNormal = endPoints[i].normal;
921+
for( int j = 1; j <= smoothness; j++ )
922+
{
923+
Vector3 tangent = tuple.GetTangent( j * _1OverSmoothness ).normalized;
924+
prevNormal = Vector3.Cross( tangent, Vector3.Cross( prevNormal, tangent ).normalized ).normalized;
925+
}
926+
927+
if( i < endPoints.Count - 1 )
928+
endPoints[i + 1].normal = prevNormal;
929+
else if( prevNormal != -endPoints[0].normal )
930+
endPoints[0].normal = ( endPoints[0].normal + prevNormal ).normalized;
911931
}
912932

913-
if( loop )
933+
// Rotate normals
934+
for( int i = 0; i < endPoints.Count; i++ )
914935
{
915-
Vector3 point0Normal2 = CalculateFrenetNormal( endPoints.Count - 1, 0, ONE_MINUS_DELTA_T, 1f, endPoints[0].autoCalculatedNormalAngleOffset );
916-
if( flipNormals )
917-
point0Normal2 = -point0Normal2;
936+
float rotateAngle = normalAngle + endPoints[i].autoCalculatedNormalAngleOffset;
937+
if( Mathf.Approximately( rotateAngle, 180f ) )
938+
endPoints[i].normal = -endPoints[i].normal;
939+
else if( !Mathf.Approximately( rotateAngle, 0f ) )
940+
{
941+
if( i < endPoints.Count - 1 )
942+
tuple = new PointIndexTuple( this, i, i + 1, 0f );
943+
else if( loop )
944+
tuple = new PointIndexTuple( this, i, 0, 0f );
945+
else
946+
tuple = new PointIndexTuple( this, i - 1, i, 1f );
918947

919-
if( point0Normal2 != -endPoints[0].normal )
920-
endPoints[0].normal = ( endPoints[0].normal + point0Normal2 ).normalized;
948+
endPoints[i].normal = Quaternion.AngleAxis( rotateAngle, tuple.GetTangent() ) * endPoints[i].normal;
949+
}
921950
}
922951
}
923952

924-
private Vector3 CalculateFrenetNormal( int pointIndex1, int pointIndex2, float localT1, float localT2, float rotateAngle )
925-
{
926-
PointIndexTuple tuple = new PointIndexTuple( this, pointIndex1, pointIndex2, 0f );
927-
Vector3 tangent1 = tuple.GetTangent( localT1 ).normalized;
928-
Vector3 tangent2 = tuple.GetTangent( localT2 ).normalized;
929-
Vector3 cross = Vector3.Cross( tangent2, tangent1 ).normalized;
930-
if( Mathf.Approximately( cross.sqrMagnitude, 0f ) ) // This is not a curved spline but rather a straight line
931-
cross = Vector3.Cross( tangent2, ( tangent2.x != 0f || tangent2.z != 0f ) ? Vector3.up : Vector3.forward );
932-
933-
Vector3 normal = Vector3.Cross( cross, tangent1 ).normalized;
934-
if( rotateAngle != 0f )
935-
normal = Quaternion.AngleAxis( rotateAngle, tangent1 ) * normal;
936-
937-
return normal;
938-
}
939-
940953
private float AccuracyToStepSize( float accuracy )
941954
{
942955
if( accuracy <= 0f )

Plugins/BezierSolution/Editor/BezierUtils.cs

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -264,21 +264,6 @@ public static void DrawSplineInspectorGUI( BezierSpline[] splines )
264264
EditorGUI.indentLevel--;
265265
}
266266

267-
EditorGUI.showMixedValue = HasMultipleDifferentValues( splines, ( s1, s2 ) => s1.Internal_FlipAutoCalculatedNormals == s2.Internal_FlipAutoCalculatedNormals );
268-
EditorGUI.BeginChangeCheck();
269-
bool flipAutoCalculatedNormals = EditorGUILayout.Toggle( "Flip Auto Calculated Normals", splines[0].Internal_FlipAutoCalculatedNormals );
270-
if( EditorGUI.EndChangeCheck() )
271-
{
272-
for( int i = 0; i < splines.Length; i++ )
273-
{
274-
Undo.RecordObject( splines[i], "Change Normals Flip" );
275-
splines[i].Internal_FlipAutoCalculatedNormals = flipAutoCalculatedNormals;
276-
splines[i].Internal_SetDirtyImmediatelyWithUndo( "Change Normals Flip" );
277-
}
278-
279-
SceneView.RepaintAll();
280-
}
281-
282267
EditorGUI.showMixedValue = false;
283268

284269
EditorGUI.BeginChangeCheck();
@@ -297,6 +282,23 @@ public static void DrawSplineInspectorGUI( BezierSpline[] splines )
297282
SceneView.RepaintAll();
298283
}
299284

285+
EditorGUI.showMixedValue = HasMultipleDifferentValues( splines, ( s1, s2 ) => s1.Internal_AutoCalculatedNormalsAngle == s2.Internal_AutoCalculatedNormalsAngle );
286+
EditorGUI.BeginChangeCheck();
287+
float autoCalculatedNormalsAngle = EditorGUILayout.FloatField( "Auto Calculated Normals Angle", splines[0].Internal_AutoCalculatedNormalsAngle );
288+
if( EditorGUI.EndChangeCheck() )
289+
{
290+
for( int i = 0; i < splines.Length; i++ )
291+
{
292+
Undo.RecordObject( splines[i], "Change Normals Angle" );
293+
splines[i].Internal_AutoCalculatedNormalsAngle = autoCalculatedNormalsAngle;
294+
splines[i].Internal_SetDirtyImmediatelyWithUndo( "Change Normals Angle" );
295+
}
296+
297+
SceneView.RepaintAll();
298+
}
299+
300+
EditorGUI.showMixedValue = false;
301+
300302
EditorGUILayout.Space();
301303

302304
GUI.color = AUTO_CONSTRUCT_SPLINE_BUTTON_COLOR;

0 commit comments

Comments
 (0)