개발 · 컴퓨터공학/three.js / / 2024. 7. 19. 03:28

Threejs Cloth Tailor 개발일지 - mesh 분리, mesh 자르기 아이디어 : spring cut off, mesh clone copy, mesh 분리 cloth simulation

728x90
반응형

 

이렇게 잘린 경우 일반적으로는 mesh가 분리되어야한다

하지만 현재는 geometry만 움직이고 있고 사실은 같은 mesh에 속한다 

 

mesh를 분리하려면 어떻게 코드를 수정해야할지 좀 봐야겠다. 

 

raycast event 처리

일단 raycast event 중복 현상 등을 빠르기 해결하자. 이벤트의 잘못된 구조를 없애자. 

 

export function init(scene: Scene, camera: Camera): Raycaster{
  // set mouse status
  window.addEventListener('mousedown', ()=>{isMouseDown = true}, false)
  window.addEventListener('mouseup', ()=>{isMouseDown = false}, false)
  
  window.addEventListener('mousemove', (event)=>{
    onMouseMove(event)
    switch(mode.curMode){
      case "NONE": break;
      case "RAYCAST":
        viewIntersectPoint(scene, camera)
        break;
      case "REMOVE_VERTEX":
        if(isMouseDown){
          viewIntersectPoint(scene, camera)

          // remove clicked vertex
          const clickMesh: Mesh = getIntersectObject(scene, camera)!
          if(clickMesh !== null) {
            const vertexIndex = getIntersectVertex(scene, camera)[0]
            removeFace(clickMesh, vertexIndex)
          }
        }
        break;
      case "REMOVE_EDGE": 
        if(isMouseDown){
          console.log('removing edge')
          stackClickVertexIndex(scene, camera)
        }
        break;
      default: break;
    }
  }, false)

  window.addEventListener('mousedown', (event)=>{
    switch(mode.curMode){
      case "NONE": break;
      case "RAYCAST": break;
      case "REMOVE_VERTEX":
        viewIntersectPoint(scene, camera)
          
        // remove clicked vertex
        const clickMesh: Mesh = getIntersectObject(scene, camera)!
        if(clickMesh !== null) {
          const vertexIndex = getIntersectVertex(scene, camera)[0]
          removeFace(clickMesh, vertexIndex)
        }
        break;
      case "REMOVE_EDGE": 
        cuttingVertexIndexList = [] // initialize vertex index list
        break;
      default: break;
    }
  }, false)

  window.addEventListener('mouseup', (event)=>{
    switch(mode.curMode){
      case "NONE": break;
      case "RAYCAST":
        scene.remove(gizmoLine)
        break;
      case "REMOVE_VERTEX":
        scene.remove(gizmoLine)
        break;
      case "REMOVE_EDGE":
        const clickMesh: Mesh = getIntersectObject(scene, camera)!
        // if not null cut along the edge
        if(clickMesh !== undefined && clickMesh !== null){
          edgeCut(clickMesh, cuttingVertexIndexList)
        }
        scene.remove(gizmoLine)
        break;
      default:
        break;
    }
  }, false)

  return raycaster
}

이렇게 하나의 init을 통해서 그냥 FSM(유한 상태 기계) 상태처럼 mode case를 분리해서

이벤트를 처리했다.

 

이제 중복 현상 같은 건 안 생기겠지.

다음은 mesh 분리다. 

mesh 분리

remove edge의 경우 온전한 알고리즘이 아니라서 일 자 형태의 경우에나 버그없이 잘 잘린다.

그래서 주로 remove vertex를 사용할 예정이다.

 

어쨌든 간에 geometry가 온전히 두 개로 분리되면 mouse up을 하는 시점에 분리된 geometry들을 서로 다른 mesh로 만들어서 scene에 각각 추가해주는 로직을 생각하고 있다.

 

mesh가 분리되었다는 사실은 사용자는 인지하지 못하는데, 이건 개발하는 입장에서도 hierarchy gui가 있으면 좋다.

이 부분은 좀 고려해보자. 

 

※ 수정이 필요한 코드
export function separateMesh(mesh: THREE.Mesh){
  // BufferGeometry의 속성 추출
  const positionAttribute = mesh.geometry.getAttribute('position') as THREE.BufferAttribute;
  const indexAttribute = mesh.geometry.getIndex() as THREE.BufferAttribute;
  
  // vertex 좌표 추출
  const vertices: THREE.Vector3[] = [];
  for (let i = 0; i < positionAttribute.count; i++) {
      vertices.push(new THREE.Vector3(
          positionAttribute.getX(i),
          positionAttribute.getY(i),
          positionAttribute.getZ(i)
      ));
  }

  // face 인덱스 추출
  const faces: number[][] = [];
  for (let i = 0; i < indexAttribute.count; i += 3) {
      faces.push([
          indexAttribute.getX(i),
          indexAttribute.getX(i + 1),
          indexAttribute.getX(i + 2)
      ]);
  }

  // 각 vertex의 인접 face를 기록
  const adjacencyList = new Map();
  faces.forEach((face, index) => {
      face.forEach(vertexIndex => {
          if (!adjacencyList.has(vertexIndex)) {
              adjacencyList.set(vertexIndex, []);
          }
          adjacencyList.get(vertexIndex).push(index);
      });
  });

  // 방문한 face를 기록하기 위한 배열
  const visitedFaces = new Array(faces.length).fill(false);

  // 깊이 우선 탐색(DFS)을 이용하여 연결된 face 그룹 찾기
  function findConnectedFaces(faceIndex: number, group: number[]) {
      const stack = [faceIndex];
      while (stack.length > 0) {
          const currentFaceIndex: number = stack.pop()!;
          if (visitedFaces[currentFaceIndex]) continue;
          visitedFaces[currentFaceIndex] = true;
          group.push(currentFaceIndex);

          const face = faces[currentFaceIndex];
          face.forEach(vertexIndex => {
              adjacencyList.get(vertexIndex).forEach(adjacentFaceIndex => {
                  if (!visitedFaces[adjacentFaceIndex]) {
                      stack.push(adjacentFaceIndex);
                  }
              });
          });
      }
  }

  const groups = [];
  for (let i = 0; i < faces.length; i++) {
      if (!visitedFaces[i]) {
          const group = [];
          findConnectedFaces(i, group);
          groups.push(group);
      }
  }

  // 각 그룹을 새로운 BufferGeometry로 분리
  const separatedGeometries = groups.map(group => {
      const newGeometry = new THREE.BufferGeometry();
      const newVertices = [];
      const newIndices = [];
      const vertexMapping = new Map();
      let newIndex = 0;

      group.forEach(faceIndex => {
          const face = faces[faceIndex];
          face.forEach(vertexIndex => {
              if (!vertexMapping.has(vertexIndex)) {
                  vertexMapping.set(vertexIndex, newIndex++);
                  const vertex = vertices[vertexIndex];
                  newVertices.push(vertex.x, vertex.y, vertex.z);
              }
              newIndices.push(vertexMapping.get(vertexIndex));
          });
      });

      newGeometry.setAttribute('position', new THREE.Float32BufferAttribute(newVertices, 3));
      newGeometry.setIndex(newIndices);
      newGeometry.computeVertexNormals();

      return newGeometry;
  });

  return separatedGeometries;
}

separate mesh 함수로 백트래킹을 통해 geometry group을 분리하고, 

각 다른 mesh로 포함시켜 분리하는 로직을 작성하고 있다. 

 

mesh 기준 group을 나누어야 하므로 face이 기준으로 코드를 생성해서 작업한다. 

 

export function separateMesh(mesh: THREE.Mesh){
  // BufferGeometry의 속성 추출
  const positionAttribute = mesh.geometry.getAttribute('position') as THREE.BufferAttribute;
  const indexAttribute = mesh.geometry.getIndex() as THREE.BufferAttribute;
  
  // vertex 좌표 추출
  const vertices: THREE.Vector3[] = [];
  for (let i = 0; i < positionAttribute.count; i++) {
      vertices.push(new THREE.Vector3(
          positionAttribute.getX(i),
          positionAttribute.getY(i),
          positionAttribute.getZ(i)
      ));
  }

  // face 인덱스 추출
  const faces: number[][] = [];
  for (let i = 0; i < indexAttribute.count; i += 3) {
      faces.push([
          indexAttribute.getX(i),
          indexAttribute.getX(i + 1),
          indexAttribute.getX(i + 2)
      ]);
  }

  // 각 vertex의 인접 face를 기록
  const adjacencyList = new Map();
  faces.forEach((face, index) => {
      face.forEach(vertexIndex => {
          if (!adjacencyList.has(vertexIndex)) {
              adjacencyList.set(vertexIndex, []);
          }
          adjacencyList.get(vertexIndex).push(index);
      });
  });

  // 방문한 face를 기록하기 위한 배열
  const visitedFaces = new Array(faces.length).fill(false);

  // 깊이 우선 탐색(DFS)을 이용하여 연결된 face 그룹 찾기
  function findConnectedFaces(faceIndex: number, group: number[]) {
      const stack = [faceIndex];
      while (stack.length > 0) {
          const currentFaceIndex: number = stack.pop()!;
          if (visitedFaces[currentFaceIndex]) continue;
          visitedFaces[currentFaceIndex] = true;
          group.push(currentFaceIndex);

          const face = faces[currentFaceIndex];
          face.forEach((vertexIndex: number) => {
            const adjacentFaces = adjacencyList.get(vertexIndex);
            if (adjacentFaces) {
                adjacentFaces.forEach((adjacentFaceIndex: number) => {
                    if (!visitedFaces[adjacentFaceIndex]) {
                        stack.push(adjacentFaceIndex);
                    }
                });
            }
        });
      }
  }

  const groups = [];
  for (let i = 0; i < faces.length; i++) {
      if (!visitedFaces[i]) {
          const group: number[] = [];
          findConnectedFaces(i, group);
          groups.push(group);
      }
  }

  // 각 그룹을 새로운 BufferGeometry로 분리
  const separatedGeometries = groups.map(group => {
      const newGeometry = new THREE.BufferGeometry();
      const newVertices: number[] = [];
      const newIndices: number[] = [];
      const vertexMapping = new Map();
      let newIndex = 0;

      group.forEach(faceIndex => {
          const face = faces[faceIndex];
          face.forEach(vertexIndex => {
              if (!vertexMapping.has(vertexIndex)) {
                  vertexMapping.set(vertexIndex, newIndex++);
                  const vertex = vertices[vertexIndex];
                  newVertices.push(vertex.x, vertex.y, vertex.z);
              }
              newIndices.push(vertexMapping.get(vertexIndex));
          });
      });

      newGeometry.setAttribute('position', new THREE.Float32BufferAttribute(newVertices, 3));
      newGeometry.setIndex(newIndices);
      newGeometry.computeVertexNormals();

      return newGeometry;
  });

  return separatedGeometries;
}

1차적으로 작성한 mesh 분리 함수인데.

정상적으로 실행되는지 scene childern의 mesh를 통해 알아보아야한다.

 

현재는 floor, cloth, gizmo mesh들이 들어있다.

저 cloth mesh가 분리되는 것이 목표.

 

과정을 보니까 분리는 되는데, scene에 추가를 안하고 있었다 ;;

 

clone copy

분리를 할 때 name이나 uuid를 제외한 material 등 다른 mesh의 컴포넌트들은 일치해야한다.

따라서 복사를 해야하는데. threejs에서는 항상 clone와 copy가 헷갈린다. 

 

stack overflow 질문들을 보니 clone에는 recursive하게 복사하는 기능이 있는 모양이다. 

일반적으로 deep copy에는 clone을 사용한다.

 

단, mesh를 clone하면 material과 geometry는 직접 넣어야한다는데? 

 

mesh 분리 cloth simulation

  separatedGeometries.forEach((geometry, index) =>{
    const newMesh: THREE.Mesh = mesh.clone();
    newMesh.geometry = geometry;
    newMesh.name = mesh.name+index

    scene.add(newMesh)
  })

  scene.remove(mesh)
}

separate의 마지막에 이렇게 분리한 새로운 mesh들을 추가하고, 

본래 mesh는 지웠다.

 

그랬더니 시뮬레이션 렌더링이 안되는 모습이 보이는데.. 

예상하는 것은 새로운 mesh가 main update 함수에서 simulation되는 각각의 cloth 객체가 되지 못해서 그런 것 같다.

 

이 부분은 생각못했네.. mesh 분리하는 것이 의미가 없었나 잠시 생각한다.

하지만 mesh object로 분리하지 않는다면, translation에서 막히는데? 

mesh 분리는 어쨌든 해야하는 것이다.

 

function simulationStart(){
  cloth = new Cloth(currentMesh, thickness, true)

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

  // set floor height
  cloth.setFloorHeight(floorHeight)
}

분리된 각각의 mesh를 scene에서 각자 cloth 객체에 넣고 시뮬레이션 하도록 하자.

 

지금 cloth 객체가 전역으로 설정된게 문제인데. 

이걸 mesh마다 따로 할당하고 사용할 수 있도록 해야한다. 

 

cloth 객체가 mesh를 갖고 있도록 함으로써, 

앞으로는 mesh단위가 아니라 cloth 객체 단위로 다루도록 해보자 .

 

function simulationStart(){
  cloth = new Cloth(currentMesh, thickness, true)

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

  // set floor height
  cloth.setFloorHeight(floorHeight)
}

그리고 이 simulation 함수가 문제다

currentMesh하나만을 cloth 객체에 넣고 있다. 

여러 개의 cloth 객체를 쓰려면 수정하자. 

 

이렇게 simulation할 cloth 객체들은 따로 리스트에 담아 simulation 시키기로 했다

 

94줄에서 에러?

여긴데 TS에서 에러도 안나고 parsing해서 가져온 mesh를 잘 담을텐데 왜그러지

아.. cloth 객체를 안 생성했구나..

 

그럼 코드를 이렇게 수정해야한다.

Cloth 객체를 생성하면서, parsing한 return mesh를 넣기.

 

마지막으로 selected cloth 설정도 처리해주니

 

다시 잘 나온다. 

 

근데 시뮬레이션이 왜이러지?

constraint가 안된다. 아무래도 simulation start할 때 cloth를 새로 생성하기 때문인 것 같다. 

 

function simulationStart(){
  simulClothList.forEach(cloth => {
    cloth.registerDistanceConstraint(0.0)
    cloth.registerPerformantBendingConstraint(1.0)
    cloth.registerSelfCollision()
    // cloth.registerIsometricBendingConstraint(10.0)
  
    // set floor height
    cloth.setFloorHeight(floorHeight)
  })
}

그럼 그렇지. 새로 cloth를 선언한 부분만 제거하니 제대로 나온다.

 

이제 mesh 분리했을 때 시뮬레이션이 안되는 문제를 해결하자. 

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