Verlet integration is used to simulate cloth.
Some optimizations:
1. Mutable Vector Operations (biggest impact)
The original code created new arrays for every add3(), sub3(), scale3() call
Now using pre-allocated tempVec1, tempVec2, tempVec3 that get reused
2. Squared Distance Comparisons
Sorting triangles by distance no longer calls sqrt() - uses squared distances instead
en.wikipedia.org/wiki/verlet_integration
#physics #cloth
Log in to post a comment.
// Optimized Cloth Simulation
// Original by Reinder Nijhoff 2019
// Optimized version with:
// - Mutable vector operations (no array allocations)
// - Cached calculations (normals, distances)
// - Squared distance comparisons (avoiding sqrt)
// - Pre-allocated arrays
// - Reduced object creation
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
// Pre-allocated vectors for reuse
const tempVec1 = [0,0,0];
const tempVec2 = [0,0,0];
const tempVec3 = [0,0,0];
// Mutable vector operations (modify first argument in place)
function scale3m(out, a, b) {
out[0] = a[0]*b; out[1] = a[1]*b; out[2] = a[2]*b;
return out;
}
function add3m(out, a, b) {
out[0] = a[0]+b[0]; out[1] = a[1]+b[1]; out[2] = a[2]+b[2];
return out;
}
function sub3m(out, a, b) {
out[0] = a[0]-b[0]; out[1] = a[1]-b[1]; out[2] = a[2]-b[2];
return out;
}
function len3(a) { return Math.sqrt(a[0]*a[0]+a[1]*a[1]+a[2]*a[2]); }
function len3sq(a) { return a[0]*a[0]+a[1]*a[1]+a[2]*a[2]; }
function dot3(a,b) { return a[0]*b[0]+a[1]*b[1]+a[2]*b[2]; }
function normalize3m(out, a) {
const l = 1/Math.sqrt(a[0]*a[0]+a[1]*a[1]+a[2]*a[2]);
out[0] = a[0]*l; out[1] = a[1]*l; out[2] = a[2]*l;
return out;
}
function cross3m(out, a, b) {
out[0] = a[1]*b[2]-a[2]*b[1];
out[1] = a[2]*b[0]-a[0]*b[2];
out[2] = a[0]*b[1]-a[1]*b[0];
return out;
}
// Standard verlet integration with optimizations
class point {
constructor(p, mass) {
this.p = [...p];
this.p_prev = [...p];
this.mass = mass / dot3(p, p);
this.acc = [0, -9.8, 0];
this.links = [];
}
attach(p2, stiffness) {
this.links.push(new link(this, p2, stiffness));
}
update(dt) {
// Reuse temp vectors instead of allocating
sub3m(tempVec1, this.p, this.p_prev); // vel
scale3m(tempVec1, tempVec1, .9); // damping
scale3m(tempVec2, this.acc, dt*dt/2);
add3m(tempVec2, tempVec2, tempVec1);
add3m(tempVec2, tempVec2, this.p);
// Update positions
this.p_prev[0] = this.p[0];
this.p_prev[1] = this.p[1];
this.p_prev[2] = this.p[2];
this.p[0] = tempVec2[0];
this.p[1] = tempVec2[1];
this.p[2] = tempVec2[2];
}
solveCollisions() {
if (shape == 0) { // collision with sphere
if (len3sq(this.p) <= 1) {
normalize3m(this.p, this.p_prev);
this.p_prev[0] = this.p[0];
this.p_prev[1] = this.p[1];
this.p_prev[2] = this.p[2];
}
} else { // collision with block
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[0] = p[0];
this.p[1] = Math.max(.9, Math.min(1, p[1]+.01));
this.p[2] = p[2];
this.p_prev[0] = this.p[0];
this.p_prev[1] = this.p[1];
this.p_prev[2] = this.p[2];
}
}
}
solve() {
this.solveCollisions();
for (let i = 0; i < this.links.length; i++) {
this.links[i].solve();
}
}
}
class link {
constructor(p1, p2, stiffness) {
this.p1 = p1;
this.p2 = p2;
this.stiffness = stiffness;
// Cache the initial distance
sub3m(tempVec1, p1.p, p2.p);
this.distance = len3(tempVec1);
this.massRatio = (1/p1.mass) / (1/p1.mass + 1/p2.mass);
}
solve() {
sub3m(tempVec1, this.p1.p, this.p2.p);
const d = len3(tempVec1);
const diff = (this.distance - d)/d;
const stiffDiff = this.stiffness * diff;
// Apply corrections directly
const s = this.massRatio * stiffDiff;
this.p1.p[0] += tempVec1[0] * s;
this.p1.p[1] += tempVec1[1] * s;
this.p1.p[2] += tempVec1[2] * s;
const t = (1-this.massRatio) * stiffDiff;
this.p2.p[0] -= tempVec1[0] * t;
this.p2.p[1] -= tempVec1[1] * t;
this.p2.p[2] -= tempVec1[2] * t;
}
}
class world {
constructor() {
this.points = [];
}
addPoint(p) {
this.points.push(p);
return p;
}
update(dt, relax) {
const pts = this.points;
const len = pts.length;
// Update all points
for (let i = 0; i < len; i++) {
pts[i].update(dt);
}
// Constraint solving iterations
for (let j = 0; j < relax; j++) {
for (let i = 0; i < len; i++) {
pts[i].solve();
}
}
}
}
// Cloth with optimizations
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;
// Create points
for (let x = 0; x < w; x++) {
ps[x] = [];
for (let y = 0; y < h; y++) {
const p = new point(
[(x-(w-1)/2)*s + center[0], center[1], (y-(h-1)/2)*s + center[2]],
mass
);
ps[x][y] = world.addPoint(p);
}
}
const stiffness = .95;
const stiffness2 = .1;
// Create links
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);
}
}
// Create triangles with neighbor references
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, 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));
}
}
}
}
// Optimized triangle rendering
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;
this.normal = null;
this.dist = null;
}
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) => {
if (!t2) return false;
const n1 = t1.getNormal();
const n2 = t2.getNormal();
sub3m(tempVec1, t2.p1.p, cameraPos);
sub3m(tempVec2, t1.p1.p, cameraPos);
return Math.sign(dot3(n2, tempVec1)) == Math.sign(dot3(n1, tempVec2));
};
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() {
if (this.dist !== null) return this.dist;
// Use squared distances to avoid sqrt
sub3m(tempVec1, this.p1.p, cameraPos);
sub3m(tempVec2, this.p2.p, cameraPos);
sub3m(tempVec3, this.p3.p, cameraPos);
return this.dist = Math.max(
len3sq(tempVec1),
len3sq(tempVec2),
len3sq(tempVec3)
);
}
getNormal() {
if (this.normal) return this.normal;
sub3m(tempVec1, this.p1.p, this.p3.p);
sub3m(tempVec2, this.p2.p, this.p3.p);
this.normal = [0,0,0];
cross3m(this.normal, tempVec1, tempVec2);
return this.normal;
}
}
const w = new world();
const r = new cloth(w, 77, 53, .1, [0,1,0]);
let seed = 1000;
w.points.sort((a,b) => 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 using cached distances
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);
}
// Original vec3 functions kept for compatibility
function scale3(a,b) { return [a[0]*b,a[1]*b,a[2]*b]; }
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 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) {
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) {
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
////////////////////////////////////////////////////////////////
function Polygons() {
const polygonList = [];
const Polygon = class {
constructor() {
this.cp = [];
this.dp = [];
this.aabb = [];
}
addPoints(...points) {
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) {
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;
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;
}
boolean(p, diff = true) {
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;
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];
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) {
if (diff === !p.inside(ls0)) {
ndp.push(ls0, ls1);
}
} else {
int.push(ls0, ls1);
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;
}
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
////////////////////////////////////////////////////////////////
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);
}
function random() {
const bits = 20;
const mod = 1<<bits;
let r = 1103515245 * (((seed+=12345) >> 1) ^ seed);
r = 1103515245 * (r ^ (r >> 3));
r = r ^ (r >> 16);
return (r % mod) / mod;
}