Skip to content

Instantly share code, notes, and snippets.

@sketchpunk
Last active November 22, 2018 02:14
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save sketchpunk/01d6c377b3923fc6117664477c1142ac to your computer and use it in GitHub Desktop.
Save sketchpunk/01d6c377b3923fc6117664477c1142ac to your computer and use it in GitHub Desktop.
Bezier Spline and Arc Length
import Vec3 from "/fungi/maths/Vec3.js";
class Bezier{
static get(p0, p1, p2, p3, t, out){
let i = 1 - t,
ii = i * i,
iii = ii * i,
tt = t * t,
ttt = tt * t,
iit3 = 3 * ii * t,
itt3 = 3 * i * tt;
out = out || new Vec3();
//out.x = i * i * i * p0.x + 3 * i * i * t * p1.x + 3 * i * t * t * p2.x + t * t * t * p3.x;
//out.y = i * i * i * p0.y + 3 * i * i * t * p1.y + 3 * i * t * t * p2.y + t * t * t * p3.y;
//out.z = i * i * i * p0.z + 3 * i * i * t * p1.z + 3 * i * t * t * p2.z + t * t * t * p3.z;
out.x = iii * p0.x + iit3 * p1.x + itt3 * p2.x + ttt * p3.x;
out.y = iii * p0.y + iit3 * p1.y + itt3 * p2.y + ttt * p3.y;
out.z = iii * p0.z + iit3 * p1.z + itt3 * p2.z + ttt * p3.z;
return out;
}
/* First Derivative represents Curve's Tangent or the Speed Direction*/
static derivative(p0, p1, p2, p3, t, out){ // bigO( 31 )
//Clamp t betwen 0 and 1
if(t > 1) t = 1;
else if(t < 0) t = 0;
var i = 1 - t,
ii3 = 3 * i * i,
it6 = 6 * i * t,
tt3 = 3 * t * t;
out = out || new Vec3();
//out.x = 3 * i * i * (p1.x - p0.x) + 6 * i * t * (p2.x - p1.x) + 3 * t * t * (p3.x - p2.x);
//out.y = 3 * i * i * (p1.y - p0.y) + 6 * i * t * (p2.y - p1.y) + 3 * t * t * (p3.y - p2.y);
//out.z = 3 * i * i * (p1.z - p0.z) + 6 * i * t * (p2.z - p1.z) + 3 * t * t * (p3.z - p2.z);
out.x = ii3 * (p1.x - p0.x) + it6 * (p2.x - p1.x) + tt3 * (p3.x - p2.x);
out.y = ii3 * (p1.y - p0.y) + it6 * (p2.y - p1.y) + tt3 * (p3.y - p2.y);
out.z = ii3 * (p1.z - p0.z) + it6 * (p2.z - p1.z) + tt3 * (p3.z - p2.z);
return out;
}
//https://stackoverflow.com/questions/35901079/calculating-the-inflection-point-of-a-cubic-bezier-curve
static derivative_2(p0, p1, p2, p3, t, out){ // bigO ( 31 )
//Clamp t betwen 0 and 1
if(t > 1) t = 1;
else if(t < 0) t = 0;
out = out || new Vec3();
let t6 = 6 * t;
out.x = t6 * (p3.x + 3 * (p1.x - p2.x) - p0.x) + 6 * (p0.x - 2 * p1.x + p2.x);
out.y = t6 * (p3.y + 3 * (p1.y - p2.y) - p0.y) + 6 * (p0.y - 2 * p1.y + p2.y);
out.z = t6 * (p3.z + 3 * (p1.z - p2.z) - p0.z) + 6 * (p0.z - 2 * p1.z + p2.z);
return out;
}
//
static normal(p0, p1, p2, p3, t, out){
let // Get Derivitive which we can use as its forward direction of an XYZ axis
d1 = Bezier.derivative(p0, p1, p2, p3, t).normalize(),
// can also use first derivitive with a forward offset of t, but D2's BigO is the same, so better off using D2
d2 = Bezier.derivative_2(p0, p1, p2, p3, t).normalize(),
// Create cross product for form a rotation axis.
cp = Vec3.cross(d2, d1).normalize();
// cp create a Rotation Matrix that can be applied on the first derivitive.
// The idea to rotate the derivitive 90 degrees on the axis of c
out.x = (cp.x * cp.x) * d1.x + (cp.x * cp.y - cp.z) * d1.y + (cp.x * cp.z + cp.y) * d1.z;
out.y = (cp.x * cp.y + cp.z) * d1.x + (cp.y * cp.y) * d1.y + (cp.y * cp.z - cp.x) * d1.z;
out.z = (cp.x * cp.z - cp.y) * d1.x + (cp.y * cp.z + cp.x) * d1.y + (cp.z * cp.z) * d1.z;
return out;
}
//Get the 3 axis directions for a specific spot
static axis(p0, p1, p2, p3, t, out = null){
if(! out) out = { x:new Vec3(), y:new Vec3(), z:new Vec3() };
Bezier.derivative(p0, p1, p2, p3, t, out.z).normalize();
let d2 = Bezier.derivative_2(p0, p1, p2, p3, t).normalize();
Vec3.cross(d2, out.z, out.x).normalize();
let cp = out.x, //Short cuts
d1 = out.z;
out.y[0] = (cp.x * cp.x) * d1.x + (cp.x * cp.y - cp.z) * d1.y + (cp.x * cp.z + cp.y) * d1.z;
out.y[1] = (cp.x * cp.y + cp.z) * d1.x + (cp.y * cp.y) * d1.y + (cp.y * cp.z - cp.x) * d1.z;
out.y[2] = (cp.x * cp.z - cp.y) * d1.x + (cp.y * cp.z + cp.x) * d1.y + (cp.z * cp.z) * d1.z;
out.y.normalize();
return out;
}
static splineAt(ary, t, out){
var i;
out = out || new Vec3();
//Final Curve in the spline
//This only work if norm is less then 1, 1 can screw things up so this condition prevents things from breaking.
if(t >= 1){
t = 1;
i = ary.length - 4;
}else{ //Determine which curve is being accessed.
if(t < 0) t = 0;
/* NOTE TO SELF
The idea is to multiple t by total curves in the spline. This gives you a whole number and fraction. Ex : t = 0.6 cnt = 2, results 1.2
The result gives both the curve index to access and the normal to apply to it. So for the example we need to access the
second segment, so by flooring we strip out the decimal we get the value 1 (second curve where 0 would be the first based on indexes).
If we take the T minus the Index to get the decimal which also works as the new normal that can be applied to the second curve.
From there we just need to multiple by 3 to get the starting index of the curve.
*/
//determine which curve we're accessing by using the norm times curve count, Must be less then 1 to work.
t *= (ary.length - 1) * 0.33333333333; // divide by 3
i = Math.floor(t); //Curve index by stripping out the decimal
t -= i; //Strip out the whole number to get the decimal norm to be used for the curve
i *= 3; //Each curve starts at the 4th point in the array, so times 3 gets us the index where the curve starts.
}
return Bezier.get( ary[i], ary[i+1], ary[i+2], ary[i+3], t, out );
}
static splineDerivativeAt(ary, t, out){
var i;
out = out || new Vec3();
//Final Curve in the spline
//This only work if norm is less then 1, 1 can screw things up so this condition prevents things from breaking.
if(t >= 1){
t = 1;
i = ary.length - 4;
}else{ //Determine which curve is being accessed.
if(t < 0) t = 0;
//determine which curve we're accessing by using the norm times curve count, Must be less then 1 to work.
t *= (ary.length - 1) * 0.33333333333; // divide by 3
i = Math.floor(t); //Curve index by stripping out the decimal
t -= i; //Strip out the whole number to get the decimal norm to be used for the curve
i *= 3; //Each curve starts at the 4th point in the array, so times 3 gets us the index where the curve starts.
}
return Bezier.derivative( ary[i], ary[i+1], ary[i+2], ary[i+3], t, out );
}
static applySplineMidControl(ary, a, b, c, scale){
//http://scaledinnovation.com/analytics/splines/aboutSplines.html
let lenBA = ary[a].length( ary[b] ), // Length of M to A
lenBC = ary[c].length( ary[b] ), // Length of M to B
lenACi = 1 / (lenBA + lenBC), // Total Length of MA+MB inverted
scaleA = scale * lenBA * lenACi, // Using the lengths, normalize it
scaleB = scale * lenBC * lenACi,
deltaAC = Vec3.sub( ary[c], ary[a] ); // Slope of A and B, used as the line for the mid control pnts
ary[b].sub( Vec3.scale(deltaAC, scaleA), ary[b-1] );
ary[b].add( Vec3.scale(deltaAC, scaleB), ary[b+1] );
}
}
// https://gist.github.com/BonsaiDen/670236
// https://gamedev.stackexchange.com/questions/5373/moving-ships-between-two-planets-along-a-bezier-missing-some-equations-for-acce/5427#5427
class BezierArcLength{
constructor(v0,v1,v2,v3, sampleCnt=null){
this.v0 = new Vec3( v0 );
this.v1 = new Vec3( v1 );
this.v2 = new Vec3( v2 );
this.v3 = new Vec3( v3 );
this.samples = null; // Mapping of t to arc length to that point on curve.
this.sampleCnt = 0; // Count of samples
this.sampleCntInv = 0; // Avoid Division in get function
this.arcLength = 0; // approximation of the length of the curve
if( sampleCnt ) this.createSamples( sampleCnt );
}
createSamples( cnt ){
this.samples = new Array( cnt + 1 );
this.samples[0] = 0;
this.sampleCnt = cnt;
this.sampleCntInv = 1 / cnt;
this.arcLength = 0;
//...................................
// Make an array of samples. The idea is to
// map the curve t to the arc length at that point.
// At each step, we accumulate the length of the curve
let v = new Vec3(),
vp = new Vec3( this.v0 ),
simpleInv = 1 / cnt;
for(let i=1; i <= cnt; i++){
Bezier.get( this.v0, this.v1, this.v2, this.v3,
i * simpleInv, v);
this.arcLength += v.length( vp );
this.samples[i] = this.arcLength;
vp.copy( v );
}
return this;
}
setPoints( a, b, c, d, sampleCnt=10 ){
this.v0.copy( a );
this.v1.copy( b );
this.v2.copy( c );
this.v3.copy( d );
this.createSamples( sampleCnt );
return this;
}
// The purpose of this function is to map the t of the arc length to the
// t of the curve. This will ideally allow the traversal of the curve
// based on its total length.
mapT(t){
//.......................................
// Return Extremes, else set variables
if(t <= 0) return 0;
if(t >= 1) return 1;
let targetLen = t * this.arcLength,
minIdx = 0,
min = 0,
max = this.sampleCnt;
//.......................................
// Binary Search to find the idx of a sample
// that is just below the target length so if the target is 10,
// find index that has a length at 10 or ver close on the min side,like 9
while( min < max ){
minIdx = min + ( ((max - min) * 0.5) | 0 ); //get mid index, use pipe for same op as Math.floor()
// if sample is under target, use mid index+1 as new min
// else sample is over target, use index as max for next iteration
if(this.samples[minIdx] < targetLen) min = minIdx + 1;
else max = minIdx;
}
// if by chance sample is over target because we ended up with max index, go back one.
if( this.samples[minIdx] > targetLen ) minIdx--;
//Check if the idex is within bounds
if( minIdx < 0 ) return 0; // Well, can't have less then 0 as an index :)
if( minIdx >= this.sampleCnt ) return 1; // if the max value is less then target, just return 1;
//.......................................
// Interpolate between the min and max indexes.
let minLen = this.samples[ minIdx ];
if( minLen == targetLen ) return minIdx * this.sampleCntInv; //ii = minIdx / this.sampleCnt;
// Where does the targetLen exist between the min and maxLen... this t is the
// Length interplation between the two sample points. Since Each sample point
// itself is the t of the curve. So for example,
// minIdx is 2 and we have 10 samples, So we can get the curve t by doing minIdx / SampleCnt
// Now are target leng lives between index 2 and 3... So by finding the gradient value between
// for example 0.5... So we're on index 2 but we need an extra half of index... So 2.5 sample index
// We take that value and divide - 2.5 / sampleCnt = t of curve in relation to arc length.
let maxLen = this.samples[ minIdx + 1 ],
tLen = (targetLen - minLen) / ( maxLen - minLen );
//console.log(t, minIdx, tLen, ( minIdx + tLen ) * this.sampleCntInv);
return ( minIdx + tLen ) * this.sampleCntInv;
//return (minIdx + (targetLen - minLen) / (this.samples[minIdx + 1] - minLen)) * this.sampleCntInv;
//return (minIdx + (targetLen - minLen) / (this.samples[minIdx + 1] - minLen)) / this.sampleCnt;
}
getVec(v, t){
let tt = this.mapT( t );
Bezier.get(this.v0, this.v1, this.v2, this.v3, tt, v);
return v;
}
}
export default Bezier;
export { BezierArcLength };
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment