PhysicsObject
이전에 다루어던 constraint와 collision의 solve, hashing을 이용한 preIntegration 등의 기능들이
이 physics object class에서 호출되고 관리된다
결국 시뮬레이션의 큰 흐름을 보기 위해서는 physics object class를 봐야한다
export default abstract class PhysicsObject {
...
constructor(mesh: Mesh) {
this.numParticles = mesh.positions.length / 3;
this.positions = new Float32Array(mesh.positions);
this.normals = new Float32Array(mesh.normals);
this.prevPositions = new Float32Array(mesh.positions);
this.vels = new Float32Array(3 * this.numParticles);
this.invMass = new Float32Array(this.numParticles);
this.indices = new Uint16Array(mesh.indices);
this.constraints = [];
this.collisions = [];
this.invMass = this.initInvMass();
this.neighbors = this.findTriNeighbors();
this.constraintFactory = new ConstraintFactory(
this.positions,
this.invMass,
this.indices,
this.neighbors
);
}
physics object class 자체는 추상 클래스이고
이를 extend한 cloth physics object에서 실질적으로 객체를 생성해서 사용한다
export default abstract class PhysicsObject {
...
constructor(mesh: Mesh) {
...
this.invMass = this.initInvMass();
this.neighbors = this.findTriNeighbors();
생성자에서는 대표적으로
역질량들을 구하는 initInvMass와
이웃 삼각형을 구하는 findTriNeighbors를 호출한다
두 함수에 대해서 알아보자
initInvMass 역질량 구하기
역질량은 constraint 수식에서 조정해야할 position 변화량을 구할 때 반드시 들어가는 중요한 값이다
흠.. 각 particle마다의 질량은 똑같아야 되는것 아닌가? 하는 의문이 든다
아무래도 삼각형 mesh 단위로 질량을 구하기 때문에 서로 다를 수 있는 것 같다
private initInvMass(): Float32Array {
const invMass = new Float32Array(this.numParticles);
const numTris = this.indices.length / 3;
const e0 = [0.0, 0.0, 0.0]; // edge 0 vector
const e1 = [0.0, 0.0, 0.0]; // edge 1 vector
const c = [0.0, 0.0, 0.0]; // cross vector of e0 x e1
for (let i = 0; i < numTris; i++) {
const id0 = this.indices[3 * i];
const id1 = this.indices[3 * i + 1];
const id2 = this.indices[3 * i + 2];
...
임의의 edge 0 1과 두 edge를 외적한 c를 정의한다
각 삼각형마다 인덱스를 가져와서
private initInvMass(): Float32Array {
...
for (let i = 0; i < numTris; i++) {
...
// Find Area of Triangle
// Calculate edge vectors from id0
vecSetDiff(e0, 0, this.positions, id1, this.positions, id0);
vecSetDiff(e1, 0, this.positions, id2, this.positions, id0);
// Area of triangle 1/2 |AB x AC|
vecSetCross(c, 0, e0, 0, e1, 0);
const A = 0.5 * Math.sqrt(vecLengthSquared(c, 0)); // magnitude of cross vector
// Divide mass among 3 points in triangle
const pInvMass = A > 0.0 ? 1.0 / A / 3.0 : 0.0;
// Add since vertices may be shared
invMass[id0] += pInvMass;
invMass[id1] += pInvMass;
invMass[id2] += pInvMass;
}
index와 position을 이용해서 삼각형을 이루는 두 edge를 구하고
두 edge를 외적한 결과 c로
삼각형의 면적 \(A\)를 구한다
면적 \(A\)를 세 점으로 고르게 나누어서 \(\frac{1}{3A}\)로 각 point의 질량을 구한다
구한 point 질량을 삼각형을 이루는 particle id들에 추가한다
이렇게 하면 특정 id particle의 역질량은
한 점이 이루고 있는 여러 삼각형의 질량들 대한 누적값을 갖게 될 것이다
findTriNeighbors 주변 삼각형 구하기
주변 삼각형이라 함은 같은 edge를 공유하는 삼각형을 말하는 것일까?
private findTriNeighbors(): Float32Array {
const edges = [];
const numTris = this.indices.length / 3;
for (let i = 0; i < numTris; i++) {
for (let j = 0; j < 3; j++) {
const id0 = this.indices[3 * i + j];
const id1 = this.indices[3 * i + ((j + 1) % 3)];
edges.push({
id0: Math.min(id0, id1), // particle 1
id1: Math.max(id0, id1), // particle 2
edgeNr: 3 * i + j, // global edge number
});
}
}
...
우선 한 삼각형의 edge를 이루는 두 particle id 0 1을 edges라는 배열에 순서쌍으로 넣는다
private findTriNeighbors(): Float32Array {
...
// sort so common edges are next to each other
edges.sort((a, b) =>
a.id0 < b.id0 || (a.id0 == b.id0 && a.id1 < b.id1) ? -1 : 1
);
// find matching edges
const neighbors = new Float32Array(3 * numTris);
neighbors.fill(-1); // -1 means open edge, as in no neighbors
let i = 0;
while (i < edges.length) {
const e0 = edges[i];
const e1 = edges[i + 1];
// If the particles share the same edge, update the neighbors list
// with their neighbors corresponding global edge number
if (e0.id0 === e1.id0 && e0.id1 === e1.id1) {
neighbors[e0.edgeNr] = e1.edgeNr;
neighbors[e1.edgeNr] = e0.edgeNr;
}
i += 2;
}
return neighbors;
}
그리고 edge들을 id 크기의 오름차순으로 정렬하고
정렬한 edge들을 순회하면서 edge0과 edge1의 particle id가 서로 같은지 본다
id가 둘 다 같다면 같은 edge라는 말인데
이렇게 같은 edge를 공유하는 삼각형을 찾아서 neighbors에 등록한다
그럼 neighbors에는 edge0과 같은 edge가 edge1 임을 알 수 있는 것이다
즉 처음의 예상과는 달리 이웃되는 삼각형의 공유되는 edge에 대한 값이었다
draw에서 물리 시뮬레이션의 역할
gravity[2] = Math.cos(Date.now() / 2000) * 15.5;
cloth.preIntegration(sdt);
for (let i = 0; i < steps; i++) {
cloth.preSolve(sdt, gravity);
cloth.solve(sdt);
cloth.postSolve(sdt);
}
위 코드는 draw 함수에서 physics object 가 하는 역할들이다
physics object를 상속받은 cloth 객체에서는
먼저 preIntegration을하고
다음 preSolve, solve, postSolve 세 단계를 거친다
먼저 세 가지 solve 과정에대해서 알아보자
preSolve
preSolve(dt: number, gravity: Float32Array) {
for (let i = 0; i < this.numParticles; i++) {
if (this.invMass[i] == 0.0) continue;
vecAdd(this.vels, i, gravity, 0, dt);
const v = Math.sqrt(vecLengthSquared(this.vels, i));
const maxV = 0.2 * (0.01 / dt);
if (v > maxV) {
vecScale(this.vels, i, maxV / v);
}
vecCopy(this.prevPositions, i, this.positions, i);
vecAdd(this.positions, i, this.vels, i, dt);
}
}
pre solve는 말 그대로 solve 과정 전에 처리되는 작업들이다
질량이 있다면 particle의 각 축 방향에 대한 속도로 속력 \(v\)를 구한다
max velocity인 maxV를 정해놓고, 속력이 그 이상 올라가면 조정한다
이전 position은 prev로 저장하고,
최종적으로 구한 속력으로 새로운 position을 구해서 적용한다
solve
solve(dt: number) {
for (let i = 0; i < this.numParticles; i++) {
// Floor collision ( we currently don't have a need for it)
let y = this.positions[3 * i + 1];
const height = -0.7;
if (y < height) {
vecCopy(this.positions, i, this.prevPositions, i);
this.positions[3 * i + 1] = height;
}
}
for (const constraint of this.constraints) {
constraint.solve(dt);
}
for (const collision of this.collisions) {
collision.solve(dt);
}
}
solve는 매 프레임(delta time)마다 세 가지 과정을 거친다
- y축 방향으로 바닥을 설정하여 더 내려가지 못하도록 하는 과정
- constraint solve
- collision solve
세 가지 과정이 순서대로 적용된다
constraint solve는 distance / performant / isometric 세 개의 contraint에 각각 따로 solve가 정의되어있는데
이전 포스팅을 통해 자세히 알아보도록 하자
collision의 경우에는 다음 포스팅을 참고하자
postSolve
postSolve(dt: number) {
for (let i = 0; i < this.numParticles; i++) {
if (this.invMass[i] == 0.0) continue;
vecSetDiff(
this.vels,
i,
this.positions,
i,
this.prevPositions,
i,
1.0 / dt
);
}
}
postSolve는 반대로 solve 과정 이후 후처리 작업이다
질량이 있는 particle에 대해
실질적으로 움직인 position 변화량으로 velocity를 update한다
마무리
preSolve, solve, postSolve 이외에도 physics object에서는
draw call 과정에서 필요한 물리 시뮬레이션 업데이트 과정의 함수인
update vertex normals이 있고
물리 시뮬레이션 업데이트 과정 전에도 constraint를 초기화하는 함수들이 있다
다음 포스팅에서 이에 대해 알아보자