Skip to content

Instantly share code, notes, and snippets.

@rlkelly
Created December 19, 2020 16:22
Show Gist options
  • Save rlkelly/af0aa6dc745c63d4162343b35aceadf4 to your computer and use it in GitHub Desktop.
Save rlkelly/af0aa6dc745c63d4162343b35aceadf4 to your computer and use it in GitHub Desktop.
Catenary in THREE.js
const start = {x: -10, y: 3};
const end = {x: 10, y: 5};
let segCnt = 40,
ropeLen = 30;
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera( 75, window.innerWidth/window.innerHeight, 0.1, 1000 );
camera.position.z = 50
var renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, window.innerHeight );
renderer.shadowMapEnabled = true;
var ambientLight = new THREE.AmbientLight(0x0c0c0c);
scene.add(ambientLight);
var spotLight = new THREE.SpotLight(0xffffff);
spotLight.position.set(60, 60, -60);
spotLight.castShadow = true;
scene.add(spotLight);
document.body.appendChild( renderer.domElement );
var render = function () {
requestAnimationFrame( render );
renderer.render(scene, camera);
};
class Vec2 extends Float32Array{
constructor(ini) {
super(2);
if(ini instanceof Vec2 || (ini && ini.length == 2)){ this[0] = ini[0]; this[1] = ini[1]; }
else if(arguments.length == 2){ this[0] = arguments[0]; this[1] = arguments[1]; }
else{ this[0] = this[1] = ini || 0; }
}
get x(){ return this[0]; } set x(val){ this[0] = val; }
get y(){ return this[1]; } set y(val){ this[1] = val; }
set(x,y) { this[0] = x; this[1] = y; return this;}
clone(){ return new Vec2(this); }
copy(v){ this[0] = v[0]; this[1] = v[1]; return this; }
nearZero(x = 1e-6,y = 1e-6){
if(Math.abs(this[0]) <= x) this[0] = 0;
if(Math.abs(this[1]) <= y) this[1] = 0;
return this;
}
length(v){
if(v === undefined) return Math.sqrt( this[0]*this[0] + this[1]*this[1] );
var x = this[0] - v[0],
y = this[1] - v[1];
return Math.sqrt( x*x + y*y );
}
lengthSqr(v){
if(v === undefined) return this[0]*this[0] + this[1]*this[1];
var x = this[0] - v[0],
y = this[1] - v[1];
return x*x + y*y;
}
normalize(out = null){
var mag = Math.sqrt( this[0]*this[0] + this[1]*this[1] );
if(mag == 0) return this;
out = out || this;
out[0] = this[0] / mag;
out[1] = this[1] / mag;
return out;
}
lerp(v, t, out){
out = out || this;
var tMin1 = 1 - t;
out[0] = this[0] * tMin1 + v[0] * t;
out[1] = this[1] * tMin1 + v[1] * t;
return out;
}
rotate(ang, out){
out = out || this;
var cos = Math.cos(ang),
sin = Math.sin(ang),
x = this[0],
y = this[1];
out[0] = x * cos - y * sin;
out[1] = x * sin + y * cos;
return out;
}
invert(out = null){
out = out || this;
out[0] = -this[0];
out[1] = -this[1];
return out;
}
add(v, out=null){
out = out || this;
out[0] = this[0] + v[0];
out[1] = this[1] + v[1];
return out;
}
addXY(x, y, out=null){
out = out || this;
out[0] = this[0] + x;
out[1] = this[1] + y;
return out;
}
sub(v, out=null){
out = out || this;
out[0] = this[0] - v[0];
out[1] = this[1] - v[1];
return out;
}
mul(v, out=null){
out = out || this;
out[0] = this[0] * v[0];
out[1] = this[1] * v[1];
return out;
}
div(v, out=null){
out = out || this;
out[0] = (v[0] != 0)? this[0] / v[0] : 0;
out[1] = (v[1] != 0)? this[1] / v[1] : 0;
return out;
}
scale(v, out=null){
out = out || this;
out[0] = this[0] * v;
out[1] = this[1] * v;
return out;
}
divInvScale(v, out=null){
out = out || this;
out[0] = (this[0] != 0)? v / this[0] : 0;
out[1] = (this[1] != 0)? v / this[1] : 0;
return out;
}
floor(out=null){
out = out || this;
out[0] = Math.floor( this[0] );
out[1] = Math.floor( this[1] );
return out;
}
static add(a,b,out){
out = out || new Vec2();
out[0] = a[0] + b[0];
out[1] = a[1] + b[1];
return out;
}
static sub(a, b, out){
out = out || new Vec2();
out[0] = a[0] - b[0];
out[1] = a[1] - b[1];
return out;
}
static scale(v, s, out = null){
out = out || new Vec2();
out[0] = v[0] * s;
out[1] = v[1] * s;
return out;
}
static dot(a,b){ return a[0] * b[0] + a[1] * b[1]; }
static floor(v, out=null){
out = out || new Vec2();
out[0] = Math.floor( v[0] );
out[1] = Math.floor( v[1] );
return out;
}
static fract(v, out=null){
out = out || new Vec2();
out[0] = v[0] - Math.floor( v[0] );
out[1] = v[1] - Math.floor( v[1] );
return out;
}
static length(v0,v1){
var x = v0[0] - v1[0],
y = v0[1] - v1[1];
return Math.sqrt( x*x + y*y );
}
static lerp(v0, v1, t, out){
out = out || new Vec2();
var tMin1 = 1 - t;
out[0] = v0[0] * tMin1 + v1[0] * t;
out[1] = v0[1] * tMin1 + v1[1] * t;
return out;
}
}
function getPoints(pntA, pntB) {
const catPoints = [];
let prevPnt = {x: start.x, y: start.y};
let A = catenary.getA(pntA, pntB, ropeLen);
let dist = pntB.length( pntA ), // Length between Two Points
distHalf = dist * 0.5, // ... Half of that
segInc = dist / segCnt, // Size of Each Segment
offset = catenary(A, -distHalf), // First C on curve, use it as an Offset to align everything.
pnt = new Vec2(),
xpos, c, i;
let y; //todo not need, only for testing inverting the sag
for(i=1; i < segCnt; i++){
Vec2.lerp(pntA, pntB, i / segCnt, pnt);
y = pnt.y; // only for inverting testing, throw away if only want downward sag
xpos = i * segInc - distHalf; // x position between two points but using half as zero center
c = catenary(A, xpos); // get a y value, but needs to be changed to work with coord system.
pnt[1] -= (offset - c); // Current lerped Y minus C of starting point minus current C
catPoints.push(prevPnt);
prevPnt = {x: pnt.x, y: pnt.y};
}
catPoints.push({x: pntB[0], y: pntB[1]})
return catPoints;
};
function catenary(a, x){ return a * Math.cosh( x / a ); }
catenary.getA = function(vec0, vec1, ropeLen){
//Solving A comes from : http://rhin.crai.archi.fr/rld/plugin_details.php?id=990
let yDelta = vec1[1] - vec0[0],
vecLen = vec1.length(vec0);
if(yDelta > ropeLen || vecLen > ropeLen){ console.log("not enough rope"); return null; }
if(yDelta < 0){ //Swop verts, low end needs to be on the left side
var tmp = vec0;
vec0 = vec1;
vec1 = vec0;
yDelta *= -1;
}
//....................................
const max_tries = 100;
let vec3 = new Vec2( vec1[0], vec0[1] ),
e = Number.MAX_VALUE,
a = 100,
aTmp = 0,
yRopeDelta = 0.5 * Math.sqrt(ropeLen*ropeLen - yDelta*yDelta), //Optimize the loop
vecLenHalf = 0.5 * vecLen, //Optimize the loop
i;
for(i=0; i < max_tries; i++){
//aTmp = 0.5 * vecLen / ( Math.asinh( 0.5 * Math.sqrt(ropeLen**2 - yDelta**2) / a ) );
aTmp = vecLenHalf / ( Math.asinh( yRopeDelta / a ) );
e = Math.abs( (aTmp - a) / a );
a = aTmp;
if(e < 0.001) break;
}
//console.log("tries", i);
return a;
}
let pntA = new Vec2(start.x, start.y),
pntB = new Vec2(end.x, end.y);
const curve = new THREE.CatmullRomCurve3(getPoints(pntA, pntB).map(x => new THREE.Vector3(x.x, x.y, 0)));
const points = curve.getPoints(50);
const g = new THREE.BufferGeometry().setFromPoints( points );
const m = new THREE.LineBasicMaterial( { color : 0xff0000 } );
// Create the final object to add to the scene
const curveObject = new THREE.Line( g, m );
scene.add(curveObject);
render();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment