Navigating Curves

In black you can observe the tangent of the curve. That will be the normal of plane that helps to define a cross-section of the curve. As you know, one can define a plane with two vectors and a single point. We have already the point i.e. the centerline location shown by the reddish sphere. The problem is that we can define an infinite number of vectors for the plane. This is a problem if we want to maintain consistency between frames in a sequence of points.

Look at the effect of navigating a curve using different frame by focusing on the red arrow. Feel free to rotate the volume to see the effect when using different frames.

The original Frenet frame defined by Frenet-Serret formulas are undefined on areas where no curvature exists. In order to avoid this an extended frenet-frame as proposed by Klok et al. can be used. It allows for consecutive frames to not rotate too much and also defined in areas where the curvature was not existent. Even so, the rotation along the centerline on points "far" apart was to great. For the purpose of this exercise, the curve is parametrized in the range from 0 to 1. The tVal is the value from the slider above. Therefore by calling getPoint() and getTangent() we can get the tangent and position at any t of the curve.

Frenet-Serret

The Frenet-Serret formulas are well known. A straight-forward issue can be observed directly in the code for the definition of N. It is based on the derivative of the tangent, which in this case uses a simple forward finite difference scheme. It is clear to see that when both tangent values are the same N will be 0, which will lead the Binormal (shown in red above) to be undefined. You can also observe how the red arrow rotates displayed path. If we were doing sampling the tubular structure above, one can easily see that it will lead to shifts along the image. In order to solve the problem, we can define a rotational minimizing frame.


let T = path.getTangent(tVal/100.0);
let N = path.getTangent(tVal/100.0).sub( path.getTangent( (tVal-1)/100.0   ));
N.normalize();
let B =  T.cross(N);
B.normalize();                     

Wang et al.: Rotation Minimizing Frame

Based on the paper "Computation of Rotation Minimizing Frames" by Wang et al. Ths approach fixes the issues named above. Move around the slider and observe how the red arrow remains on the same side of the tube for the whole range.

However, it depends on an initial frame, if this frame doesn't exist, then we define our own. We need a vector that is in the first coordinate frame, for that we do the cross-product of the tangent with a specific vector. Selecting a random initial location is not optimal as it leads to non-reproducible results. This vector is instead based on the coordinate axes. It may happen that the tangent and the chosen axis is parallel, and therefore the zero vector is generated. If this happens then a next axis is chosen.

The code to reproduce this in THREE.js can be found below. In the paper, the reference vectors are formed by performing a double reflection. We can compute them all from the start and just query them afterwards as each reference vector depends on the previous ones. It makes no sense to recompute the n-1 vectors each time the reference vector n is desired. To end, whenever we need them we project the reference vector unto the plane to keep the orthogonality.


let initialTangent = path.getTangent(0); 
_u0 = initialTangent.cross( new THREE.Vector3(0,0,1));               
_idx = 0;
doubleReflectionReferenceVectors = []
doubleReflectionReferenceVectors.push(_u0);

let t = 0.0;
let step = 1.0/100.0;
let i = 0;
				 
while( t <= 1.0 ){ 
     let v1 = path.getPoint(t+step).sub(path.getPoint(t));
     v1.normalize();   
     c1 = v1.dot(v1);
     r_i = doubleReflectionReferenceVectors[i];
     v1dotRi = v1.dot(r_i);
     r_iL =  r_i.sub(v1.multiplyScalar((2.0/c1)*v1dotRi));
     t_i = path.getTangent(t);
     v1dotTi = v1.dot(t_i);
     t_iL = t_i.sub(v1.multiplyScalar((2.0/c1)*v1dotTi));
     t_i1 = path.getTangent(t+step);
     v2 = t_i1.sub(t_iL);  // v2 = t_{i+1} - t_i^l
     v2.normalize();
     c2 =  v2.dot(v2);
     v2DotR_iL = v2.dot(r_iL);
     r_i1 = r_iL.sub(v2.multiplyScalar((2.0/c2)*v2DotR_iL));
     r_i1.normalize();
     doubleReflectionReferenceVectors.push(r_i1);
     t+= step;
     ++i;
 }
 
 /*and finally*/
 
let u = new THREE.Vector3();
u.copy(doubleReflectionReferenceVectors[tVal]);
let udotN = u.dot(path.getTangent(tVal/100.0));
let projected = u.sub( path.getTangent(tVal/100.0).multiplyScalar(udotN));
projected.normalize();