Created
December 19, 2020 16:22
-
-
Save rlkelly/af0aa6dc745c63d4162343b35aceadf4 to your computer and use it in GitHub Desktop.
Catenary in THREE.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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