UNPKG

@polygonjs/polygonjs

Version:

node-based WebGL 3D engine https://polygonjs.com

480 lines (422 loc) 15.3 kB
import {VOL_ID_ORDER} from './Common'; import {Mesh, BufferGeometry, Vector3, TypedArray} from 'three'; import { vecSetZero, vecAdd, vecCopy, vecDistSquared, vecDot, vecLengthSquared, vecScale, vecSetCross, vecSetDiff, matSetMult, matSetInverse, vecAddVector3, } from './SoftBodyMath'; import {tetSortPoints} from '../geometry/modules/tet/utils/tetSortPoints'; import {buildTetIds, buildTetEdgeIds} from '../geometry/modules/tet/utils/tetSoftBodyUtils'; import {Number3, Number9} from '../../types/GlobalTypes'; import {TetEmbed} from './Common'; import {Hash} from '../Hash'; import {ObjectUserData} from '../UserData'; import {SoftBodyConstraint} from './SoftBodyConstraint'; import { _SDFBox, // _SDFPlane } from '../../engine/functions/_SDFPrimitives'; import {TetSoftBodySolverSopNode} from '../../engine/nodes/sop/TetSoftBodySolver'; import {softBodyRayMarch} from './SoftBodyCollider'; export type VelocityFunction = () => Vector3; export type SDFFunction = () => number; export type SDFEvaluator = (p: Vector3) => number; interface SoftBodyOptions { node: TetSoftBodySolverSopNode; tetEmbed: TetEmbed; highResSkinning: { lookup: { spacing: number; padding: number; }; }; } const _pos = new Vector3(0, 0, 0); const _vel = new Vector3(0, 0, 0); const _velDt = new Vector3(0, 0, 0); const ONE_SIXTH = 1.0 / 6.0; const _v3array: Number3 = [0, 0, 0]; export class SoftBody { public readonly numParticles: number; public readonly numTets: number; public readonly pos: Float32Array; public readonly prevPos: TypedArray; public readonly vel: Float32Array; public readonly tetIds: number[]; public readonly edgeIds: number[]; public readonly restVol: Float32Array; public readonly edgeLengths: Float32Array; public readonly invMass: Float32Array; // public edgeCompliance: number = 0; // public volumeCompliance: number = 0; public readonly temp: Float32Array; public readonly grads: Float32Array; public readonly constraintsById: Map<number, SoftBodyConstraint> = new Map(); // public grabId: number; // public grabInvMass: number; private readonly bufferGeometry: BufferGeometry; // private numVisVerts: number; private skinningInfo: Float32Array; private highResGeometry: BufferGeometry | undefined; private highResObjectPosition: TypedArray; // private _node: TetSoftBodySolverSopNode; constructor(private options: SoftBodyOptions) { this._node = options.node; const {tetEmbed} = this.options; const {tetObject, lowResObject, highResObject} = tetEmbed; this.bufferGeometry = (lowResObject as Mesh).geometry; // physics this.numParticles = tetObject.geometry.pointsCount(); //tetMesh.verts.length / 3; this.numTets = tetObject.geometry.tetsCount(); //tetMesh.tetIds.length / 4; this.pos = this.bufferGeometry.attributes.position.array! as Float32Array; //new Float32Array(tetMesh.verts); this.prevPos = this.bufferGeometry.attributes.position.array.slice(); this.vel = new Float32Array(3 * this.numParticles); const newOrderByPoint: Map<number, number> = new Map(); tetSortPoints(tetObject.geometry, newOrderByPoint); this.tetIds = buildTetIds(tetObject.geometry, newOrderByPoint); //tetMesh.tetIds; this.edgeIds = buildTetEdgeIds(tetObject.geometry, newOrderByPoint); //tetMesh.tetEdgeIds; this.restVol = new Float32Array(this.numTets); this.edgeLengths = new Float32Array(this.edgeIds.length / 2); this.invMass = new Float32Array(this.numParticles); this.temp = new Float32Array(4 * 3); this.grads = new Float32Array(4 * 3); // this.grabId = -1; // this.grabInvMass = 0.0; this.initPhysics(); // high res object this.highResGeometry = highResObject ? (highResObject as Mesh).geometry : undefined; this.highResObjectPosition = this.highResGeometry ? this.highResGeometry.attributes.position.array : new Float32Array([]); const visVerts = this.highResObjectPosition; this.numVisVerts = visVerts.length / 3; this.skinningInfo = new Float32Array(4 * this.numVisVerts); if (highResObject) { this._computeSkinningInfo(visVerts); highResObject.userData[ObjectUserData.LOW_RES_SOFT_BODY_MESH] = lowResObject; } } private _computeSkinningInfo(visVerts: TypedArray) { // create a hash for all vertices of the visual mesh const hash = new Hash({ spacing: this.options.highResSkinning.lookup.spacing, maxNumObjects: this.numVisVerts, }); hash.create(visVerts); this.skinningInfo.fill(-1.0); // undefined const minDist = new Float32Array(this.numVisVerts); minDist.fill(Number.MAX_VALUE); const border = this.options.highResSkinning.lookup.padding; // each tet searches for containing vertices const tetCenter = new Float32Array(3) as any as Number3; const mat = new Float32Array(9) as any as Number9; const bary = new Float32Array(4); for (let i = 0; i < this.numTets; i++) { // compute bounding sphere of tet tetCenter.fill(0.0); for (let j = 0; j < 4; j++) vecAdd(tetCenter, 0, this.pos, this.tetIds[4 * i + j], 0.25); let rMax = 0.0; for (let j = 0; j < 4; j++) { const r2 = vecDistSquared(tetCenter, 0, this.pos, this.tetIds[4 * i + j]); rMax = Math.max(rMax, Math.sqrt(r2)); } rMax += border; hash.query(tetCenter, 0, rMax); if (hash.queryIds.length == 0) continue; const id0 = this.tetIds[4 * i]; const id1 = this.tetIds[4 * i + 1]; const id2 = this.tetIds[4 * i + 2]; const id3 = this.tetIds[4 * i + 3]; vecSetDiff(mat, 0, this.pos, id0, this.pos, id3); vecSetDiff(mat, 1, this.pos, id1, this.pos, id3); vecSetDiff(mat, 2, this.pos, id2, this.pos, id3); matSetInverse(mat); for (let j = 0; j < hash.queryIds.length; j++) { const id = hash.queryIds[j]; // we already have skinning info if (minDist[id] <= 0.0) continue; if (vecDistSquared(visVerts, id, tetCenter, 0) > rMax * rMax) continue; // compute barycentric coords for candidate vecSetDiff(bary, 0, visVerts, id, this.pos, id3); matSetMult(mat, bary, 0, bary, 0); bary[3] = 1.0 - bary[0] - bary[1] - bary[2]; let dist = 0.0; for (let k = 0; k < 4; k++) dist = Math.max(dist, -bary[k]); if (dist < minDist[id]) { minDist[id] = dist; this.skinningInfo[4 * id] = i; this.skinningInfo[4 * id + 1] = bary[0]; this.skinningInfo[4 * id + 2] = bary[1]; this.skinningInfo[4 * id + 3] = bary[2]; } } } } // updateMeshes() { // this.updateLowResObject(); // this.updateHighResMesh(); // } updateLowResObject() { // we still need to update the low res mesh // event if we only display the high res one, // as it may be used for raycasting // if (this.highResGeometry) { // return; // } if (!this.highResGeometry) { this.bufferGeometry.computeVertexNormals(); } this.bufferGeometry.attributes.position.needsUpdate = true; this.bufferGeometry.computeBoundingSphere(); } updateHighResMesh() { if (!this.highResGeometry) { return; } const positions = this.highResObjectPosition; let nr = 0; for (let i = 0; i < this.numVisVerts; i++) { let tetNr = this.skinningInfo[nr++] * 4; if (tetNr < 0) { nr += 3; continue; } const b0 = this.skinningInfo[nr++]; const b1 = this.skinningInfo[nr++]; const b2 = this.skinningInfo[nr++]; const b3 = 1.0 - b0 - b1 - b2; const id0 = this.tetIds[tetNr++]; const id1 = this.tetIds[tetNr++]; const id2 = this.tetIds[tetNr++]; const id3 = this.tetIds[tetNr++]; vecSetZero(positions, i); vecAdd(positions, i, this.pos, id0, b0); vecAdd(positions, i, this.pos, id1, b1); vecAdd(positions, i, this.pos, id2, b2); vecAdd(positions, i, this.pos, id3, b3); } this.highResGeometry.computeVertexNormals(); this.highResGeometry.attributes.position.needsUpdate = true; this.highResGeometry.computeBoundingSphere(); } getTetVolume(nr: number) { const id0 = this.tetIds[4 * nr]; const id1 = this.tetIds[4 * nr + 1]; const id2 = this.tetIds[4 * nr + 2]; const id3 = this.tetIds[4 * nr + 3]; vecSetDiff(this.temp, 0, this.pos, id1, this.pos, id0); vecSetDiff(this.temp, 1, this.pos, id2, this.pos, id0); vecSetDiff(this.temp, 2, this.pos, id3, this.pos, id0); vecSetCross(this.temp, 3, this.temp, 0, this.temp, 1); return vecDot(this.temp, 3, this.temp, 2) / 6.0; } initPhysics() { this.invMass.fill(0.0); this.restVol.fill(0.0); for (let i = 0; i < this.numTets; i++) { const vol = this.getTetVolume(i); this.restVol[i] = vol; const pInvMass = vol > 0.0 ? 1.0 / (vol / 4.0) : 0.0; this.invMass[this.tetIds[4 * i]] += pInvMass; this.invMass[this.tetIds[4 * i + 1]] += pInvMass; this.invMass[this.tetIds[4 * i + 2]] += pInvMass; this.invMass[this.tetIds[4 * i + 3]] += pInvMass; } for (let i = 0; i < this.edgeLengths.length; i++) { const id0 = this.edgeIds[2 * i]; const id1 = this.edgeIds[2 * i + 1]; this.edgeLengths[i] = Math.sqrt(vecDistSquared(this.pos, id0, this.pos, id1)); } } preSolve(dt: number, gravity: number[], velFunc: VelocityFunction, sdfEvaluator: SDFEvaluator) { for (let i = 0; i < this.numParticles; i++) { if (this.invMass[i] == 0.0) continue; _pos.fromArray(this.pos, i * 3); _vel.fromArray(this.vel, i * 3); this._node.setPointGlobals(_pos, _vel); // TODO: detect if velFunc depends on time, or point, // and compute it only when required const computedVel: Vector3 = velFunc(); computedVel.toArray(this.vel, i * 3); _velDt.copy(computedVel).multiplyScalar(dt); // vecAdd(this.vel, i, gravity, 0, dt); vecCopy(this.prevPos, i, this.pos, i); const stepMagnitude = _velDt.length(); const distToCollider: number = softBodyRayMarch(_pos, _vel, stepMagnitude, sdfEvaluator); if (stepMagnitude > distToCollider) { // handle collision // 1. set prevPos vecAdd(this.pos, i, this.vel, i, dt); vecCopy(this.pos, i, this.prevPos, i); // 2. update pos _vel.normalize().multiplyScalar(distToCollider); _pos.add(_vel); _pos.toArray(this.pos, i * 3); } else { // no collision vecAdd(this.pos, i, this.vel, i, dt); } } } solve( dt: number, edgeCompliance: number, volumeCompliance: number, preciseCollisions: boolean, sdfEvaluator: SDFEvaluator ) { this.solveEdges(dt, edgeCompliance, preciseCollisions, sdfEvaluator); this.solveVolumes(dt, volumeCompliance, preciseCollisions, sdfEvaluator); } postSolve(dt: number) { for (let i = 0; i < this.numParticles; i++) { if (this.invMass[i] == 0.0) continue; vecSetDiff(this.vel, i, this.pos, i, this.prevPos, i, 1.0 / dt); } } solveEdges(dt: number, compliance: number, preciseCollisions: boolean, sdfEvaluator: SDFEvaluator) { const alpha = compliance / dt / dt; for (let i = 0; i < this.edgeLengths.length; i++) { const id0 = this.edgeIds[2 * i]; const id1 = this.edgeIds[2 * i + 1]; const w0 = this.invMass[id0]; const w1 = this.invMass[id1]; const w = w0 + w1; if (w == 0.0) continue; vecSetDiff(this.grads, 0, this.pos, id0, this.pos, id1); const len = Math.sqrt(vecLengthSquared(this.grads, 0)); if (len == 0.0) continue; vecScale(this.grads, 0, 1.0 / len); const restLen = this.edgeLengths[i]; const C = len - restLen; const s = -C / (w + alpha); if (preciseCollisions) { // id0 _pos.fromArray(this.pos, id0 * 3); _vel.fromArray(this.grads, 0).multiplyScalar(s * w0); const stepMagnitude0 = _vel.length(); const distToCollider0: number = softBodyRayMarch(_pos, _vel, stepMagnitude0, sdfEvaluator); if (stepMagnitude0 > distToCollider0) { _vel.fromArray(this.grads, 0) .multiplyScalar(s * w0) .normalize() .multiplyScalar(distToCollider0); vecAddVector3(this.pos, id0, _vel); } else { vecAdd(this.pos, id0, this.grads, 0, s * w0); } // id1 _pos.fromArray(this.pos, id1 * 3); _vel.fromArray(this.grads, 0).multiplyScalar(-s * w1); const stepMagnitude1 = _vel.length(); const distToCollider1: number = softBodyRayMarch(_pos, _vel, stepMagnitude1, sdfEvaluator); if (stepMagnitude1 > distToCollider1) { _vel.fromArray(this.grads, 0) .multiplyScalar(-s * w1) .normalize() .multiplyScalar(distToCollider1); vecAddVector3(this.pos, id1, _vel); } else { vecAdd(this.pos, id1, this.grads, 0, -s * w1); } } else { vecAdd(this.pos, id0, this.grads, 0, s * w0); vecAdd(this.pos, id1, this.grads, 0, -s * w1); } } } solveVolumes(dt: number, compliance: number, preciseCollisions: boolean, sdfEvaluator: SDFEvaluator) { const alpha = compliance / dt / dt; for (let i = 0; i < this.numTets; i++) { let w = 0.0; for (let j = 0; j < 4; j++) { const id0 = this.tetIds[4 * i + VOL_ID_ORDER[j][0]]; const id1 = this.tetIds[4 * i + VOL_ID_ORDER[j][1]]; const id2 = this.tetIds[4 * i + VOL_ID_ORDER[j][2]]; vecSetDiff(this.temp, 0, this.pos, id1, this.pos, id0); vecSetDiff(this.temp, 1, this.pos, id2, this.pos, id0); vecSetCross(this.grads, j, this.temp, 0, this.temp, 1); vecScale(this.grads, j, ONE_SIXTH); w += this.invMass[this.tetIds[4 * i + j]] * vecLengthSquared(this.grads, j); } if (w == 0.0) continue; const vol = this.getTetVolume(i); const restVol = this.restVol[i]; const C = vol - restVol; const s = -C / (w + alpha); for (let j = 0; j < 4; j++) { const id = this.tetIds[4 * i + j]; const magnitude = s * this.invMass[id]; if (preciseCollisions) { _pos.fromArray(this.pos, id * 3); _vel.fromArray(this.grads, j * 3).multiplyScalar(magnitude); const stepMagnitude = _vel.length(); const distToCollider: number = softBodyRayMarch(_pos, _vel, stepMagnitude, sdfEvaluator); if (stepMagnitude > distToCollider) { _vel.fromArray(this.grads, j * 3) .multiplyScalar(magnitude) .normalize() .multiplyScalar(distToCollider); vecAddVector3(this.pos, id, _vel); } else { vecAdd(this.pos, id, this.grads, j, magnitude); } } else { vecAdd(this.pos, id, this.grads, j, magnitude); } } } } translate(offset: Vector3) { offset.toArray(_v3array); for (var i = 0; i < this.numParticles; i++) { vecAdd(this.pos, i, _v3array, 0); vecAdd(this.prevPos, i, _v3array, 0); } } velocityMult(mult: number) { for (var i = 0; i < this.numParticles; i++) { vecScale(this.vel, i, mult); } } // // // constraints // // createConstraint(index: number) { const constraint = new SoftBodyConstraint(this, index); this.constraintsById.set(constraint.id, constraint); constraint.invMass = this.invMass[index]; this.invMass[index] = 0.0; return constraint; } getConstraint(constraintId: number) { return this.constraintsById.get(constraintId); } private _constraintVel: Number3 = [0, 0, 0]; deleteConstraint(constraintId: number) { const constraint = this.constraintsById.get(constraintId); if (!constraint) { return; } if (constraint.pointIndex >= 0) { this.invMass[constraint.pointIndex] = constraint.invMass; constraint.velocity(this._constraintVel); vecCopy(this.vel, constraint.pointIndex, this._constraintVel, 0); } this.constraintsById.delete(constraintId); constraint.dispose(); } }