개발 / / 2024. 5. 26. 09:22

PBD cloth simulation 코드 분석 (draw, PhysicsObject, ClothPhysicsObject, updateVertexNormals, register constraints, Cloth class) [Threejs Cloth Tailor 개발일지]

반응형

 

 

PhysicsObject

이전에 preSolve, solve, postSolve에 대해서 보았으니 나머지 물리 시뮬레이션에 필요한 기능을 보자

update vertex normals

vertex normal은 mesh가 눈에 보여질 때 주로 사용된다

gpuCanvas.device.queue.writeBuffer(
  meshBuffers.position.data,
  0,
  cloth.positions,
  0,
  meshBuffers.position.length
);
gpuCanvas.device.queue.writeBuffer(
  meshBuffers.normals.data,
  0,
  cloth.normals,
  0,
  meshBuffers.normals.length
);

여기서는 cloth 객체의 vertex normal을 buffer에 담아서 render queue에 전달하여 어느면을 렌더링 할지를 정해준다

 

하지만 본래 carmen씨의 코드는 webGPU기반으로 작성되어있다

threejs를 사용하는 환경에서는 normal 성분이 없어도 mesh 정보만으로 렌더링 타입을 정할 수 있어서 실질적으로 개발할 때는 따로 처리할 부분이다

 

그렇다면 간단히만 보고 가도록 하자

  updateVertexNormals() {
    for (let i = 0; i < this.numParticles; i++) {
      vecSetZero(this.normals, i);
    }
    for (let i = 0; i < this.numParticles; i++) {
      const id0 = this.indices[3 * i];
      const id1 = this.indices[3 * i + 1];
      const id2 = this.indices[3 * i + 2];

      const e0 = [0, 0, 0];
      const e1 = [0, 0, 0];
      const c = [0, 0, 0];

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

      vecAdd(this.normals, id0, c, 0, 0.333);
      vecAdd(this.normals, id1, c, 0, 0.333);
      vecAdd(this.normals, id2, c, 0, 0.333);
    }
  }

우선 초기화를 하고 나면 particle 세 개 로 이루어진 삼각형의 두 edge를 가져온다

이때 삼각형의 세 vertex의 index를 활용한다

 

삼각형의 두 edge로 외적벡터를 구하면

이를 vertex 별로 크기를 나누어서 할당해준다

 

여기도 역질량을 구할 때와 마찬가지로

한 vertex에 다른 삼각형이 겹치는데 이때 겹치는 모든 삼각형 mesh의 normal 합을 담는다

Cloth Physics Object

physics object class에 들어있는 공통적인 부분을 모두 다루었다

이제는 cloth physics에만 할당된 기능들을 보자

export class ClothPhysicsObject extends PhysicsObject {
  thickness: number;
  hash: Hash;
  constructor(mesh: Mesh, thickness: number) {
    super(mesh);
    this.thickness = thickness;

    // Spacing calculated by looking into the obj file and seeing the length between two particles.
    const spacing = thickness;
    this.hash = new Hash(spacing, this.numParticles);
  }
  
  ...

기본 physics object를 상속받은 cloth physics는 hash table과 옷의 두께인 thickness를 가진다

preIntegration

  preIntegration(dt: number) {
    this.hash.create(this.positions);
    this.hash.queryAll(this.positions, ((1 / 60) * 0.2 * this.thickness) / dt);
  }

preintegration 단계에서는 hash table을 생성하고 

모든 particle에 대해서 query를 실행하여 인접 particle에 대한 id들을 정리해놓는다

 

hash query에 대한 자세한 설명은 이전에 다루었던 포스팅을 참고하자

 

 

아마 cloth의 경우 유체처럼 particle의 인접관계가 바뀌지 않으므로

query는 preIntegration에서 한 번만 수행하는 것이다

register constraint collision

  /**
   * Adds a DistanceConstraint to the Cloth physics object
   * @param compliance
   */
  public registerDistanceConstraint(compliance: number) {
    this.constraints.push(
      this.constraintFactory.createDistanceConstraint(compliance)
    );
  }

constraint를 등록할 때 compliance 즉 탄력성 상수를 input으로 입력한다

 

그러면 constraint factory에서 필요한 constraint를 생성 후 반환해서 constraints 배열에 넣는다

 

두 가지 의문

왜 굳이 constraints가 배열일 필요가 있는지와

각 constraint를 등록하는 함수들을 따로 만들었는지가 있다

 

constraint 등록 함수가 따로 있는 이유

이건 main 함수에서 cloth 객체가 어떻게 constraint 등록 함수를 호출하는지 보면 된다

 

cloth.registerDistanceConstraint(0.0);
cloth.registerPerformantBendingConstraint(1.0);
cloth.registerSelfCollision();
// cloth.registerIsometricBendingConstraint(10.0)

보면 알겠지만 compliance 상수들의 값이 다 다르다 

collision의 경우는 compliance가 없다

 

constraints가 배열인 이유

constraints 배열은 어디에 사용되냐?

physicsObject의 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 함수에서는 각 constraints와 collisions를 매 deltatime마다 순회하는 식으로 호출한다

이렇게 사용하려고 배열인 것이다

 

  /**
   * Adds a PerformantBendingConstraint to the Cloth physics object
   * @param compliance
   */
  public registerPerformantBendingConstraint(compliance: number) {
    this.constraints.push(
      this.constraintFactory.createPerformantBendingConstraint(compliance)
    );
  }

  /**
   * Adds an IsometricBendingConstraint to the Cloth physics object
   * @param compliance
   */
  public registerIsometricBendingConstraint(compliance: number) {
    this.constraints.push(
      this.constraintFactory.createIsometricBendingConstraint(compliance)
    );
  }

  /**
   * Adds a Self Collision constraint to the Cloth physics object
   */
  public registerSelfCollision() {
    this.collisions.push(
      new ClothSelfCollision(
        this.positions,
        this.prevPositions,
        this.invMass,
        this.thickness,
        this.hash
      )
    );
  }

나머지 constaints와 collision도 같은 맥락으로

constraints / collisions 배열에 push 한다

 

각 constraint를 생성할 때 사용되는 compliance는 서로 다르게 넣을 수 있는데

constraint에 따라 다르게 설정하면 조절되는 효과를 추후 알아볼 수 있겠다

Cloth class

cloth simulation과 관련된 마지막 스크립트로 

cloth physics object를 상속 받은 cloth class

 

export default class Cloth extends ClothPhysicsObject {
  constructor(mesh: Mesh, thickness: number) {
    super(mesh, thickness);
    this.init();
  }
  
  ...

이 cloth class로 생성한 객체를 가지고 main 함수에서 시뮬레이션을 처리한다

 

cloth class가 독단적으로 하는 역할은 initialize밖에 없다

  private init() {
    // Set top of cloth to have a mass of 0 to hold still
    // in order to get hanging from clothesline visual
    {
      // Variables to store top row
      let minX = Number.MAX_VALUE;
      let maxX = -Number.MAX_VALUE;
      let maxY = -Number.MAX_VALUE;

      for (let i = 0; i < this.numParticles; i++) {
        minX = Math.min(minX, this.positions[3 * i]);
        maxX = Math.max(maxX, this.positions[3 * i]);
        maxY = Math.max(maxY, this.positions[3 * i + 1]);
      }

      // Thickness of the edge to zero out(?)
      const eps = 0.000001;

      for (let i = 0; i < this.numParticles; i++) {
        const x = this.positions[3 * i];
        const y = this.positions[3 * i + 1];
        if (y > maxY - eps && (x < minX + eps || x > maxX - eps))
          // if (y > maxY - eps)
          this.invMass[i] = 0.0;
      }
    }
  }

상단에 적힌 주석 내용을 보면

Set top of cloth to have a mass of 0 to hold still
in order to get hanging from clothesline visual

옷의 최상단 한 줄은 빨랫줄처럼 고정되어서 시뮬레이션 되어야하기 때문에

질량을 0으로 고정시킨다는 말이다

 

  // Variables to store top row
  let minX = Number.MAX_VALUE;
  let maxX = -Number.MAX_VALUE;
  let maxY = -Number.MAX_VALUE;

  for (let i = 0; i < this.numParticles; i++) {
    minX = Math.min(minX, this.positions[3 * i]);
    maxX = Math.max(maxX, this.positions[3 * i]);
    maxY = Math.max(maxY, this.positions[3 * i + 1]);
  }

그래서 minX, maxX, maxY 세 개의 limit 수를 지정해서

position의 가장 끝 부분들을 담는다

(minxY는 이미 solve에서 floor colllision을 구현했으므로 필요 없다)

 

  // Thickness of the edge to zero out(?)
  const eps = 0.000001;

  for (let i = 0; i < this.numParticles; i++) {
    const x = this.positions[3 * i];
    const y = this.positions[3 * i + 1];
    if (y > maxY - eps && (x < minX + eps || x > maxX - eps))
      // if (y > maxY - eps)
      this.invMass[i] = 0.0;
  }

particle의 x,y 위치가 min / max 범위 이상으로 넘어가면

해당 particle의 역질량을 0으로 설정한다

 

근데 이게 모든 particle을 순회하면서 가장 윗단에 있는 particle을 찾는 방식인 것 같다

if 조건을 보면

y값은 최상단에 있으면서 x에 대한 조건도 있는데

x에 대한 조건은 뭐지? 하는 생각을 하고 있었다

고민중...

뒤늦게서야 이해했다

거의 0에 가까운 값인 eps가 하는 역할은 max, min보다 아주 조금 덜미치는 값을 설정해서

y축의 최상단에 있으면서, x축의 양쪽 끝에 있는 두 개의 particle만을 찾기 위함이었다

 

양쪽 끝 두 particle의 역질량을 0 즉 질량을 무한대로 만들어 고정시켜서

마치 옷에 빨래집게를 집어놓은 듯한 효과를 준다

이제 구현 단계로 넘어가자

cloth simulation에 들어가는 물리 시뮬레이션 관련 코드들을 모두 숙지했다

이제 이 webGPU기반 코드를 threejs로 포팅해서 테스트 해보도록 하자

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