Initializing 3D Canvas...

Normals And Binormals

2 min read1 page

Knowing the Tangent (the Forward direction) of a curve is not enough to build 3D geometry. Imagine sweeping a flat 2D rectangle along a roller coaster track. The Tangent tells the rectangle which way to face, but it doesn't tell it how much to "roll" or "bank" side-to-side!

Degrees of Freedom:

1. A single Forward vector leaves 1 degree of freedom undefined: the rotation around that vector (the Roll). 2. To lock down the orientation perfectly, we need a Reference Frame: a full set of 3 orthogonal axes (X, Y, Z). 3. The Forward vector is usually Z. We need to figure out exactly which way Y (Up) and X (Right) should point at every step along the curve.
python
1# Extruding a Shape Along a Curve
2def sweep_profile(profile_polygon, curve):
3 """How to make a pipe or ribbon."""
4
5 mesh = Mesh()
6
7 for t in range(0, 1.0, 0.05):
8 # 1. We need a coordinate system at every step
9 frame = get_reference_frame(curve, t)
10
11 # 2. We align our 2D shape to that coordinate system
12 aligned_profile = align_to_frame(profile_polygon, frame)
13
14 # 3. Add to mesh and connect to the previous step...
15 mesh.add_loop(aligned_profile)
16
17 return mesh
Sweep (Tangent Only/Twisted/Framed)
0.00
2 min read1 page

We found the Tangent (Velocity) by taking the First Derivative. To find the Normal (Up), we take the Second Derivative. In physics, the derivative of Velocity is Acceleration.

The Pull of the Curve:

1. Imagine driving a car along a curved road. 2. Velocity points straight ahead through your windshield (Tangent). 3. Acceleration is the G-Force you feel pushing you into the side of your seat as you turn! 4. The Second Derivative vector always points directly towards the "inside" or "center" of the curve's bend. This gives us our Up vector!
python
1# Calculating the Second Derivative (Acceleration)
2def get_acceleration(p0, p1, p2, t):
3 """Calculates the physical acceleration (pull) of the curve."""
4
5 # 1. Take the mathematical derivative of the First Derivative formula!
6 # For a Quadratic Bezier, the 2nd derivative formula is CONSTANT:
7 # 2 * (p2 - 2*p1 + p0)
8
9 accel_vector = 2 * (p2 - (2 * p1) + p0)
10
11 return accel_vector
Parameter t (0.0 - 1.0)
0.50
2 min read1 page

By combining the First Derivative (Velocity) and the Second Derivative (Acceleration), we finally have enough information to build a complete 3D coordinate system! This is the legendary Frenet-Serret Frame.

Cross Product Magic:

1. We take Velocity and normalize it. This is our Z-Axis (Forward). 2. We take Acceleration and normalize it. This is our Y-Axis (Up). 3. We now have two axes locked in, but we are missing the X-Axis (Right). 4. We use the Cross Product. If you cross-multiply the Z and Y axes, it mathematically guarantees a third vector that is exactly 90 degrees to both of them. This gives us our X-Axis!
python
1# Calculating the Frenet-Serret Frame
2def get_frenet_frame(curve, t):
3 """The standard math formula for a moving coordinate system."""
4
5 # 1. Forward Vector (Tangent)
6 vel = curve.get_velocity(t)
7 T = vel.normalize()
8
9 # 2. Up Vector (Normal)
10 accel = curve.get_acceleration(t)
11 N = accel.normalize()
12
13 # 3. Right Vector (Binormal)
14 # The Cross Product finds a 3rd vector 90 degrees to both T and N.
15 B = T.cross(N).normalize()
16
17 return ReferenceFrame(T, N, B)
Play Animation
1.00
2 min read1 page

The Frenet-Serret frame is mathematically perfect, but for 3D modeling, it is a disaster! If you try to sweep a rectangle using a pure Frenet frame, the resulting ribbon will often snap and twist violently.

The Flipping Normal:

1. The Normal (Up axis) always points to the inside of the curve. 2. If a curve bends Left, the Normal points Left. 3. If the curve suddenly changes direction and bends Right, the inside of the curve is now on the Right! 4. The Normal axis instantly flips 180 degrees at the inflection point (the moment it switches from Left to Right). 5. This causes extreme Torsion (twisting) in the geometry.
python
1# The Inflection Point Problem
2def sweep_frenet(curve):
3 """Why pure math fails in 3D modeling."""
4
5 mesh = Mesh()
6
7 for t in range(0, 1.0, 0.01):
8 # 1. Get the pure mathematical Frenet frame
9 frame = get_frenet_frame(curve, t)
10
11 # 2. What happens if the curve bends Left, and then bends Right?
12 # The Acceleration vector (Up axis) instantly flips 180 degrees
13 # at the inflection point between the two curves!
14
15 # 3. If we sweep a shape along this frame, the shape will
16 # violently snap upside-down exactly at that point.
17 mesh.add_profile(frame)
18
19 return mesh
Play Animation
1.00
3 min read1 page

To fix the flipping problem of the Frenet frame, the graphics industry uses the Parallel Transport Frame (also called the Bishop Frame).

Twist-Free Propagation:

1. Instead of recalculating the math from scratch at every point, we propagate the frame iteratively. 2. We start with a valid frame at t = 0. 3. As we move slightly forward, the Tangent bends by a certain angle. 4. We simply take our old Normal and Binormal vectors, and rotate them by that exact same angle! 5. Because we ignore the Second Derivative (Acceleration) entirely, inflection points are completely ignored. The frame glides smoothly without ever flipping!
python
1# Parallel Transport Frame (Bishop Frame)
2def get_bishop_frame(curve, t_steps):
3 """A twist-free frame for 3D modeling."""
4
5 frames = []
6
7 # 1. Calculate the initial frame manually at t=0
8 current_frame = get_initial_frame(curve)
9 frames.append(current_frame)
10
11 for t in t_steps[1:]:
12 # 2. Get the new Forward direction (Tangent)
13 new_tangent = curve.evaluate_derivative(t).normalize()
14
15 # 3. Calculate the angle and rotation axis between the
16 # old Tangent and the new Tangent
17 axis = current_frame.tangent.cross(new_tangent).normalize()
18 angle = math.acos(current_frame.tangent.dot(new_tangent))
19
20 # 4. Rotate the old Normal and Binormal by that exact amount!
21 # This completely ignores Acceleration, preventing flipping.
22 new_normal = current_frame.normal.rotate(axis, angle)
23 new_binormal = current_frame.binormal.rotate(axis, angle)
24
25 current_frame = ReferenceFrame(new_tangent, new_normal, new_binormal)
26 frames.append(current_frame)
27
28 return frames
Play Animation
1.00
2 min read1 page

The Bishop frame is great because it has zero twisting. But what if we want our ribbon to twist? What if we want the start of a roller coaster track to be flat, but the end of the track to be banked 90 degrees?

Guided Sweeps:

1. Because the Bishop frame provides a mathematically stable baseline, we can use it as a foundation. 2. We allow the user to provide a custom "Up Vector" at the start of the curve, and a custom "Up Vector" at the end of the curve. 3. We calculate the difference in rotation (twist) between the two. 4. As we move along the curve (increasing t), we smoothly interpolate that twist and apply it on top of the Bishop frame.
python
1# Interpolating User Up-Vectors
2def sweep_with_guides(curve, start_up, end_up):
3 """Controlling twist manually using quaternions."""
4
5 # 1. Get the Bishop Frame (Zero Twist)
6 frames = get_bishop_frame(curve)
7
8 for i in range(len(frames)):
9 t = i / (len(frames) - 1)
10 frame = frames[i]
11
12 # 2. How much should we twist at this percentage?
13 # We blend smoothly between the User's Start and End rotations
14 current_twist = slerp(start_up, end_up, t)
15
16 # 3. Apply the twist to the Bishop frame
17 frame.normal = frame.normal.rotate(frame.tangent, current_twist)
18 frame.binormal = frame.binormal.rotate(frame.tangent, current_twist)
19
20 return frames
End Twist (Degrees)
0.00
1 min read1 page

To understand why Differential Geometry is essential for 3D engine programmers, we can look at the two methods side-by-side.

Frenet vs Bishop:

1. Frenet (Red): Watch what happens at the inflection point in the middle of the S-Curve. The geometry instantly twists itself inside-out. 2. Bishop (Green): The geometry smoothly flows through the inflection point without any torsion. 3. The Bishop frame (Parallel Transport) is the standard method used in game engines (for particle trails, tire tracks, and hair) and CAD engines (for sweeping pipes and molding).
python
1# Comparing Ribbon Sweeps
2def generate_ribbons(curve):
3 """Why we care about this math."""
4
5 # 1. Frenet Sweep (Mathematically pure, visually broken)
6 frenet_ribbon = sweep_profile(profile, get_frenet_frames(curve))
7
8 # 2. Bishop Sweep (Visually perfect, mathematically relaxed)
9 bishop_ribbon = sweep_profile(profile, get_bishop_frames(curve))
10
11 return frenet_ribbon, bishop_ribbon
Algorithm (Frenet / Bishop)
0.00