개발 / / 2024. 5. 25. 09:09

PBD cloth simulation 코드 분석 (draw, PhysicsObject, ClothPhysicsObject, preSolve, solve, postSolve) [Threejs Cloth Tailor 개발일지]

반응형

 

 

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를 초기화하는 함수들이 있다

 

다음 포스팅에서 이에 대해 알아보자

반응형
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유