Verlet integration is used to simulate cloth.
The code is not optimized (and very slow) and self-intersection of the cloth is not prevented.
en.wikipedia.org/wiki/verlet_integration
#physics #cloth
Log in to post a comment.
// Cloth. Created by Reinder Nijhoff 2019 // @reindernijhoff // // https://turtletoy.net/turtle/cfe9091ad8 // const turtle = new Slowpoke(); const cameraPos = [-7,3,8]; const cameraLookAt = [-0.1,-1.3,0]; const viewProjectionMatrix = setupCamera(cameraPos, cameraLookAt); const polygons = new Polygons(); const shape = 1; // min=0, max=1, step=1 (Sphere, Box) const simulationSteps = 175; // min=1, max=250, step=1 // standard verlet integration class point { constructor(p, mass) { this.p = [...p]; this.p_prev = [...p]; // The cloth has more weigth in the center to make the simulation more stable. this.mass = mass / dot3(this.p, this.p); this.acc = [0, -9.8, 0]; this.links = []; } attach(p2, stiffness) { this.links.push(new link(this, p2, stiffness)); } update(dt) { let vel = sub3(this.p, this.p_prev); vel = scale3(vel, .9); const p = add3(add3(this.p, vel), scale3(this.acc, dt*dt/2)); this.p_prev = this.p; this.p = p; } solveCollisions() { if (shape == 0) { // collision with sphere if (len3(this.p) <= 1) { this.p = normalize3(this.p_prev); this.p_prev = [...this.p]; } } else { // colision with block const clamp = (a,b,t) => Math.max(Math.min(b,t),a); if (Math.abs(this.p[0]) < 1 && Math.abs(this.p[2]) < 1 && this.p[1] <= 1 && this.p[1] >= .9) { const p = this.p_prev; this.p = [p[0], clamp(.9, 1, p[1]+.01), p[2]]; this.p_prev = [...this.p]; } } } solve() { this.solveCollisions(); this.links.forEach(l => l.solve()); } } class link { constructor(p1, p2, stiffness) { this.p1 = p1; this.p2 = p2; this.stiffness = stiffness; this.distance = dist3(p1.p, p2.p); } solve() { const v = sub3(this.p1.p, this.p2.p); const d = len3(v); const diff = (this.distance - d)/d; const s = (1/this.p1.mass) / (1/this.p1.mass + 1/this.p2.mass); this.p1.p = add3(this.p1.p, scale3(v, s*this.stiffness*diff)); this.p2.p = add3(this.p2.p, scale3(v, -(1-s)*this.stiffness*diff)); } } class world { constructor() { this.points = []; } addPoint(p) { this.points.push(p); return p; } update(dt, relax) { this.points.forEach(p => p.update(dt)); for (let i=0; i<relax; i++) { this.points.forEach(p => p.solve()); } } } // cloth specific code class cloth { constructor(world, w, h, s, center) { this.width = w; this.height = h; const ps = this.points = []; const tr = this.triangles = []; const trgrid = []; const mass = 1; for (let x=0; x<w; x++) { this.points[x] = []; for (let y=0; y<h; y++) { const p = new point(add3([(x-(w-1)/2)*s, 0, (y-(h-1)/2)*s], center), mass); ps[x][y] = world.addPoint(p); } } const stiffness = .95; const stiffness2 = .1; for (let x=0; x<w; x++) { for (let y=0; y<h; y++) { const p = ps[x][y]; if (y < h-1) p.attach(ps[x][y+1], stiffness); if (x < w-1) p.attach(ps[x+1][y], stiffness); if (x < w-1 && y < h-1) p.attach(ps[x+1][y+1], stiffness); if (y < h-2) p.attach(ps[x][y+2], stiffness2); if (x < w-2) p.attach(ps[x+2][y], stiffness2); if (x > 1 && y < h-2) p.attach(ps[x-2][y+2], stiffness2); } } for (let x=0; x<w-1; x++) { trgrid[x] = []; for (let y=0; y<h-1; y++) { const t1 = new triangle(0,x,y,ps[x][y],ps[x+1][y],ps[x+1][y+1]); const t2 = new triangle(1,x,y,ps[x][y],ps[x][y+1],ps[x+1][y+1]); trgrid[x][y*2+0] = t1; trgrid[x][y*2+1] = t2; tr.push(t1); tr.push(t2); } } const trg = (x, y) => (x >= 0 && x < w-1 && y >= 0 && y < h*2-1) ? trgrid[x][y] : false; for (let x=0; x<w-1; x++) { for (let y=0; y<h-1; y++) { trgrid[x][y*2+0].setNeighbours(trg(x, y*2-1), trg(x+1, y*2+1), trg(x, y*2+1)); trgrid[x][y*2+1].setNeighbours(trg(x-1, y*2+0), trg(x, y*2+2), trg(x, y*2+0)); } } } } // triangles used for visualisation class triangle { constructor(o, x, y, p1, p2, p3) { this.o = o, this.x = x, this.y = y; this.p1 = p1, this.p2 = p2, this.p3 = p3; } setNeighbours(t1, t2, t3) { this.t1 = t1, this.t2 = t2, this.t3 = t3; } draw(t) { const p1 = transform4([...this.p1.p,1], viewProjectionMatrix); const p2 = transform4([...this.p2.p,1], viewProjectionMatrix); const p3 = transform4([...this.p3.p,1], viewProjectionMatrix); const s = 50; const p = polygons.create(); p.addPoints([p1[0]/p1[3]*s, -p1[1]/p1[3]*s], [p2[0]/p2[3]*s, -p2[1]/p2[3]*s], [p3[0]/p3[3]*s, -p3[1]/p3[3]*s]); const edge = (t1, t2) => t2 && Math.sign(dot3(t2.getNormal(), sub3(t2.p1.p, cameraPos))) == Math.sign(dot3(t1.getNormal(), sub3(t1.p1.p, cameraPos))); if (this.o == 0) { if (this.x % 2 == 1 || edge(this, this.t2)) p.addSegments(p.cp[1], p.cp[2]); if (this.y % 2 == 0 || edge(this, this.t1)) p.addSegments(p.cp[0], p.cp[1]); if (edge(this, this.t3)) p.addSegments(p.cp[0], p.cp[2]); } else { if (this.x % 2 == 0 || edge(this, this.t1)) p.addSegments(p.cp[0], p.cp[1]); if (this.y % 2 == 1 || edge(this, this.t2)) p.addSegments(p.cp[1], p.cp[2]); if (edge(this, this.t3)) p.addSegments(p.cp[0], p.cp[2]); } if (this.x % 4 < 2) p.addHatching(Math.PI/4, 1); polygons.draw(t, p); } getDist() { return this.dist ? this.dist : this.dist = Math.max(Math.max(dist3(this.p1.p, cameraPos), dist3(this.p2.p, cameraPos)), dist3(this.p3.p, cameraPos)); } getNormal() { return this.normal ? this.normal : this.normal = cross3(sub3(this.p1.p, this.p3.p), sub3(this.p2.p, this.p3.p)); } } const w = new world(); const r = new cloth(w, 77, 53, .1, [0,1,0]); w.points.sort((a,b) => Math.random()-.5); // The walk function will be called until it returns false. function walk(i) { if (i < simulationSteps) { w.update(1/30, 8); turtle.jump(i*190/simulationSteps-95, 95); turtle.goto((i+1)*190/simulationSteps-95, 95); if (i == simulationSteps-1) { // sort front to back r.triangles = r.triangles.sort( (a,b) => a.getDist() - b.getDist()); } } else { r.triangles[i - simulationSteps].draw(turtle); } return i < r.triangles.length - 1 + simulationSteps; } function setupCamera(camPos, camLookat) { const viewMatrix = lookAt4m(camPos, camLookat, [0,1,0]); const projectionMatrix = perspective4m(0.25, 1); return multiply4m(projectionMatrix, viewMatrix); } // vec3 functions function scale3(a,b) { return [a[0]*b,a[1]*b,a[2]*b]; } function len3(a) { return Math.sqrt(dot3(a,a)); } function dist3(a,b) { return Math.sqrt((a[0]-b[0])**2+(a[1]-b[1])**2+(a[2]-b[2])**2); } function normalize3(a) { return scale3(a,1/len3(a)); } function add3(a,b) { return [a[0]+b[0],a[1]+b[1],a[2]+b[2]]; } function sub3(a,b) { return [a[0]-b[0],a[1]-b[1],a[2]-b[2]]; } function dot3(a,b) { return a[0]*b[0]+a[1]*b[1]+a[2]*b[2]; } function cross3(a,b) { return [a[1]*b[2]-a[2]*b[1],a[2]*b[0]-a[0]*b[2],a[0]*b[1]-a[1]*b[0]]; } // vec4 functions function transform4(a,b) { const d=new Float32Array(4); for(let c=0;4>c;c++)d[c]=b[c]*a[0]+b[c+4]*a[1]+b[c+8]*a[2]+b[c+12]; return d; } // mat4 functions function lookAt4m(a,b,d) { // pos, lookAt, up const c=new Float32Array(16); b=normalize3(sub3(a,b)); d=normalize3(cross3(d,b)); const e=normalize3(cross3(b,d)); c[0]=d[0];c[1]=e[0];c[2]=b[0];c[3]=0; c[4]=d[1];c[5]=e[1];c[6]=b[1];c[7]=0; c[8]=d[2];c[9]=e[2];c[10]=b[2];c[11]=0; c[12]=-(d[0]*a[0]+d[1]*a[1]+d[2]*a[2]); c[13]=-(e[0]*a[0]+e[1]*a[1]+e[2]*a[2]); c[14]=-(b[0]*a[0]+b[1]*a[1]+b[2]*a[2]); c[15]=1; return c; } function multiply4m(a,b) { const d=new Float32Array(16); for(let c=0;16>c;c+=4) for(let e=0;4>e;e++) d[c+e]=b[c+0]*a[0+e]+b[c+1]*a[4+e]+b[c+2]*a[8+e]+b[c+3]*a[12+e]; return d; } function perspective4m(a,b) { // fovy, aspect const c=(new Float32Array(16)).fill(0,0); c[5]=1/Math.tan(a/2); c[0]=c[5]/b; c[10]=c[11]=-1; return c; } //////////////////////////////////////////////////////////////// // Polygon Clipping utility code - Created by Reinder Nijhoff 2019 // https://turtletoy.net/turtle/a5befa1f8d //////////////////////////////////////////////////////////////// function Polygons() { const polygonList = []; const Polygon = class { constructor() { this.cp = []; // clip path: array of [x,y] pairs this.dp = []; // 2d lines [x0,y0],[x1,y1] to draw this.aabb = []; // AABB bounding box } addPoints(...points) { // add point to clip path and update bounding box let xmin = 1e5, xmax = -1e5, ymin = 1e5, ymax = -1e5; (this.cp = [...this.cp, ...points]).forEach( p => { xmin = Math.min(xmin, p[0]), xmax = Math.max(xmax, p[0]); ymin = Math.min(ymin, p[1]), ymax = Math.max(ymax, p[1]); }); this.aabb = [(xmin+xmax)/2, (ymin+ymax)/2, (xmax-xmin)/2, (ymax-ymin)/2]; } addSegments(...points) { // add segments (each a pair of points) points.forEach(p => this.dp.push(p)); } addOutline() { for (let i = 0, l = this.cp.length; i < l; i++) { this.dp.push(this.cp[i], this.cp[(i + 1) % l]); } } draw(t) { for (let i = 0, l = this.dp.length; i < l; i+=2) { t.jump(this.dp[i]), t.goto(this.dp[i + 1]); } } addHatching(a, d) { const tp = new Polygon(); tp.cp.push([-1e5,-1e5],[1e5,-1e5],[1e5,1e5],[-1e5,1e5]); const dx = Math.sin(a) * d, dy = Math.cos(a) * d; const cx = Math.sin(a) * 200, cy = Math.cos(a) * 200; for (let i = 0.5; i < 150 / d; i++) { tp.dp.push([dx * i + cy, dy * i - cx], [dx * i - cy, dy * i + cx]); tp.dp.push([-dx * i + cy, -dy * i - cx], [-dx * i - cy, -dy * i + cx]); } tp.boolean(this, false); this.dp = [...this.dp, ...tp.dp]; } inside(p) { let int = 0; // find number of i ntersection points from p to far away for (let i = 0, l = this.cp.length; i < l; i++) { if (this.segment_intersect(p, [0.1, -1000], this.cp[i], this.cp[(i + 1) % l])) { int++; } } return int & 1; // if even your outside } boolean(p, diff = true) { // bouding box optimization by ge1doot. if (Math.abs(this.aabb[0] - p.aabb[0]) - (p.aabb[2] + this.aabb[2]) >= 0 && Math.abs(this.aabb[1] - p.aabb[1]) - (p.aabb[3] + this.aabb[3]) >= 0) return this.dp.length > 0; // polygon diff algorithm (narrow phase) const ndp = []; for (let i = 0, l = this.dp.length; i < l; i+=2) { const ls0 = this.dp[i]; const ls1 = this.dp[i + 1]; // find all intersections with clip path const int = []; for (let j = 0, cl = p.cp.length; j < cl; j++) { const pint = this.segment_intersect(ls0, ls1, p.cp[j], p.cp[(j + 1) % cl]); if (pint !== false) { int.push(pint); } } if (int.length === 0) { // 0 intersections, inside or outside? if (diff === !p.inside(ls0)) { ndp.push(ls0, ls1); } } else { int.push(ls0, ls1); // order intersection points on line ls.p1 to ls.p2 const cmpx = ls1[0] - ls0[0]; const cmpy = ls1[1] - ls0[1]; int.sort( (a,b) => (a[0] - ls0[0]) * cmpx + (a[1] - ls0[1]) * cmpy - (b[0] - ls0[0]) * cmpx - (b[1] - ls0[1]) * cmpy); for (let j = 0; j < int.length - 1; j++) { if ((int[j][0] - int[j+1][0])**2 + (int[j][1] - int[j+1][1])**2 >= 0.001) { if (diff === !p.inside([(int[j][0]+int[j+1][0])/2,(int[j][1]+int[j+1][1])/2])) { ndp.push(int[j], int[j+1]); } } } } } return (this.dp = ndp).length > 0; } //port of http://paulbourke.net/geometry/pointlineplane/Helpers.cs segment_intersect(l1p1, l1p2, l2p1, l2p2) { const d = (l2p2[1] - l2p1[1]) * (l1p2[0] - l1p1[0]) - (l2p2[0] - l2p1[0]) * (l1p2[1] - l1p1[1]); if (d === 0) return false; const n_a = (l2p2[0] - l2p1[0]) * (l1p1[1] - l2p1[1]) - (l2p2[1] - l2p1[1]) * (l1p1[0] - l2p1[0]); const n_b = (l1p2[0] - l1p1[0]) * (l1p1[1] - l2p1[1]) - (l1p2[1] - l1p1[1]) * (l1p1[0] - l2p1[0]); const ua = n_a / d; const ub = n_b / d; if (ua >= 0 && ua <= 1 && ub >= 0 && ub <= 1) { return [l1p1[0] + ua * (l1p2[0] - l1p1[0]), l1p1[1] + ua * (l1p2[1] - l1p1[1])]; } return false; } }; return { list: () => polygonList, create: () => new Polygon(), draw: (turtle, p, addToVisList=true) => { for (let j = 0; j < polygonList.length && p.boolean(polygonList[j]); j++); p.draw(turtle); if (addToVisList) polygonList.push(p); } }; } //////////////////////////////////////////////////////////////// // Slowpoke utility code. Created by Reinder Nijhoff 2019 // https://turtletoy.net/turtle/cfe9091ad8 //////////////////////////////////////////////////////////////// function Slowpoke(x, y) { const linesDrawn = {}; class Slowpoke extends Turtle { goto(x, y) { const p = Array.isArray(x) ? [...x] : [x, y]; if (this.isdown()) { const o = [this.x(), this.y()]; const h1 = o[0].toFixed(2)+'_'+p[0].toFixed(2)+o[1].toFixed(2)+'_'+p[1].toFixed(2); const h2 = p[0].toFixed(2)+'_'+o[0].toFixed(2)+p[1].toFixed(2)+'_'+o[1].toFixed(2); if (linesDrawn[h1] || linesDrawn[h2]) { super.up(); super.goto(p); super.down(); return; } linesDrawn[h1] = linesDrawn[h2] = true; } super.goto(p); } } return new Slowpoke(x,y); }