Cloth

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);
}