개발 · 컴퓨터공학/three.js / / 2024. 6. 25. 09:00

Threejs Cloth Tailor 개발일지 - remove mode, 메쉬 자르기

728x90
반응형

 

새로운 모드 만들기 : remove mode

export type Mode = "NONE" | "RAYCAST" | "REMOVE"
export const Modes: Mode[] = ["NONE", "RAYCAST", "REMOVE"]

REMOVE 모드에서는 마우스 이벤트가 클릭시 제거하도록 할 것인데

RAYCAST와 이벤트를 분리하려니 좀 복잡하다 

 

export function getIntersectVertex(scene: Scene, camera: Camera): number[]{
  raycaster.setFromCamera(mouse, camera)
  const intersects = raycaster.intersectObjects(scene.children)
  let closestVertexIndices

  if (intersects.length > 0) {
    for(let i = 0; i < intersects.length; i++){
      const intersect = intersects[i]
      if(intersect.object === gizmoLine) continue

      closestVertexIndices = findClosestVertexIndex(intersect.point, intersect.object as Mesh)

      break
    }
  }
  return closestVertexIndices!
}

remove 모드에서 raycast로 클릭한 부분의 인접한 vertex가 몇 번째 index인지 알 수 있도록 구현하였다

 

이제 vertex를 제거하자

mesh에서 index array에 해당하는 vertex들을 찾아서 geometry attribute에서 단순히 삭제? 하면 안될 것 같은데

 

plane object의 geometry에서는 점 하나만 사라져도 치명적이다.

메쉬 자르기 mesh cutting 로직

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Three.js Mesh Cutting</title>
    <style>
        body { margin: 0; }
        canvas { display: block; }
    </style>
</head>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
    let scene, camera, renderer, cube, planeMesh, cutMesh;

    function init() {
        // 씬, 카메라, 렌더러 초기화
        scene = new THREE.Scene();
        camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
        renderer = new THREE.WebGLRenderer();
        renderer.setSize(window.innerWidth, window.innerHeight);
        document.body.appendChild(renderer.domElement);

        // 큐브 메쉬 생성 및 추가
        const geometry = new THREE.BoxGeometry();
        const material = new THREE.MeshBasicMaterial({ color: 0x00ff00, wireframe: true });
        cube = new THREE.Mesh(geometry, material);
        scene.add(cube);

        // 절단 평면 생성 및 추가
        const planeGeometry = new THREE.PlaneGeometry(5, 5);
        const planeMaterial = new THREE.MeshBasicMaterial({ color: 0xff0000, side: THREE.DoubleSide, wireframe: true });
        planeMesh = new THREE.Mesh(planeGeometry, planeMaterial);
        planeMesh.rotation.x = Math.PI / 2;  // 평면을 수평으로 회전
        scene.add(planeMesh);

        camera.position.z = 5;

        animate();
    }

    function animate() {
        requestAnimationFrame(animate);
        renderer.render(scene, camera);
    }

    // 메쉬 절단 함수
    function cutMeshWithPlane(mesh, plane) {
        const geometry = mesh.geometry.clone();
        geometry.computeVertexNormals();

        const positionAttribute = geometry.getAttribute('position');
        const newVertices = [];
        const newIndices = [];
        const vertexMap = new Map();

        function getVertexIndex(v) {
            const key = `${v.x},${v.y},${v.z}`;
            if (!vertexMap.has(key)) {
                vertexMap.set(key, newVertices.length / 3);
                newVertices.push(v.x, v.y, v.z);
            }
            return vertexMap.get(key);
        }

        for (let i = 0; i < positionAttribute.count; i += 3) {
            const v0 = new THREE.Vector3().fromBufferAttribute(positionAttribute, i);
            const v1 = new THREE.Vector3().fromBufferAttribute(positionAttribute, i + 1);
            const v2 = new THREE.Vector3().fromBufferAttribute(positionAttribute, i + 2);

            const d0 = plane.distanceToPoint(v0);
            const d1 = plane.distanceToPoint(v1);
            const d2 = plane.distanceToPoint(v2);

            const outside = [];
            const inside = [];

            [[v0, d0], [v1, d1], [v2, d2]].forEach(([v, d]) => {
                if (d >= 0) {
                    outside.push({ vertex: v, distance: d });
                } else {
                    inside.push({ vertex: v, distance: d });
                }
            });

            if (inside.length === 0) {
                [v0, v1, v2].forEach(v => newIndices.push(getVertexIndex(v)));
                continue;
            }

            if (outside.length === 0) {
                continue;
            }

            if (outside.length === 1 && inside.length === 2) {
                const [vA, vB, vC] = [outside[0], inside[0], inside[1]];
                const tAB = vA.distance / (vA.distance - vB.distance);
                const tAC = vA.distance / (vA.distance - vC.distance);

                const vAB = vA.vertex.clone().lerp(vB.vertex, tAB);
                const vAC = vA.vertex.clone().lerp(vC.vertex, tAC);

                newIndices.push(
                    getVertexIndex(vA.vertex),
                    getVertexIndex(vAB),
                    getVertexIndex(vAC)
                );
                newIndices.push(
                    getVertexIndex(vAB),
                    getVertexIndex(vB.vertex),
                    getVertexIndex(vC.vertex)
                );
                newIndices.push(
                    getVertexIndex(vAC),
                    getVertexIndex(vB.vertex),
                    getVertexIndex(vC.vertex)
                );
            }

            if (outside.length === 2 && inside.length === 1) {
                const [vA, vB, vC] = [inside[0], outside[0], outside[1]];
                const tAB = vA.distance / (vA.distance - vB.distance);
                const tAC = vA.distance / (vA.distance - vC.distance);

                const vAB = vA.vertex.clone().lerp(vB.vertex, tAB);
                const vAC = vA.vertex.clone().lerp(vC.vertex, tAC);

                newIndices.push(
                    getVertexIndex(vA.vertex),
                    getVertexIndex(vAB),
                    getVertexIndex(vAC)
                );
                newIndices.push(
                    getVertexIndex(vAB),
                    getVertexIndex(vB.vertex),
                    getVertexIndex(vC.vertex)
                );
            }
        }

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

        return new THREE.Mesh(newGeometry, new THREE.MeshBasicMaterial({ color: 0x0000ff, wireframe: true }));
    }

    // 메쉬를 자르는 함수 호출
    init();
    cutMesh = cutMeshWithPlane(cube, new THREE.Plane(new THREE.Vector3(0, 1, 0), 0));
    scene.add(cutMesh);

</script>
</body>
</html>

메쉬를 자르는 테스트 코드를 좀 찾아봤는데

위 코드의 경우는 plane mesh로 cube mesh를 자르는 로직이다

 

중요한 것은 메쉬의 vertex가 분리되어야할 때 어떻게 처리하냐를 보기 위함이니까 한 번 살펴보자

 

        for (let i = 0; i < positionAttribute.count; i += 3) {
            const v0 = new THREE.Vector3().fromBufferAttribute(positionAttribute, i);
            const v1 = new THREE.Vector3().fromBufferAttribute(positionAttribute, i + 1);
            const v2 = new THREE.Vector3().fromBufferAttribute(positionAttribute, i + 2);

            const d0 = plane.distanceToPoint(v0);
            const d1 = plane.distanceToPoint(v1);
            const d2 = plane.distanceToPoint(v2);

            const outside = [];
            const inside = [];

중요한건 각 mesh polygon을 하나씩 보면서 처리하는 반복문인데

v0,v1,v2 는 polygon을 이루는 세 vertex이다

 

plane에는 distance to point라고 해서 평면의 안팎을 +-거리로 측정할 수 있나보다

평면 기준으로 vertex가 어디있는지를 outside inside에 넣고

 

            if (inside.length === 0) {
                [v0, v1, v2].forEach(v => newIndices.push(getVertexIndex(v)));
                continue;
            }

            if (outside.length === 0) {
                continue;
            }

이걸 보면 inside가 원래 mesh로부터 분리되는 새로운 메쉬가 있는 공간이지 싶다

 

            if (outside.length === 1 && inside.length === 2) {
                const [vA, vB, vC] = [outside[0], inside[0], inside[1]];
                const tAB = vA.distance / (vA.distance - vB.distance);
                const tAC = vA.distance / (vA.distance - vC.distance);

                const vAB = vA.vertex.clone().lerp(vB.vertex, tAB);
                const vAC = vA.vertex.clone().lerp(vC.vertex, tAC);

                newIndices.push(
                    getVertexIndex(vA.vertex),
                    getVertexIndex(vAB),
                    getVertexIndex(vAC)
                );
                newIndices.push(
                    getVertexIndex(vAB),
                    getVertexIndex(vB.vertex),
                    getVertexIndex(vC.vertex)
                );
                newIndices.push(
                    getVertexIndex(vAC),
                    getVertexIndex(vB.vertex),
                    getVertexIndex(vC.vertex)
                );
            }

            if (outside.length === 2 && inside.length === 1) {
                const [vA, vB, vC] = [inside[0], outside[0], outside[1]];
                const tAB = vA.distance / (vA.distance - vB.distance);
                const tAC = vA.distance / (vA.distance - vC.distance);

                const vAB = vA.vertex.clone().lerp(vB.vertex, tAB);
                const vAC = vA.vertex.clone().lerp(vC.vertex, tAC);

                newIndices.push(
                    getVertexIndex(vA.vertex),
                    getVertexIndex(vAB),
                    getVertexIndex(vAC)
                );
                newIndices.push(
                    getVertexIndex(vAB),
                    getVertexIndex(vB.vertex),
                    getVertexIndex(vC.vertex)
                );
            }
        }

두 조건을 보면 vertex 하나가 안에있고 나머지가 밖에있거나 그 반대의 경우를 체크한다 

즉 mesh가 plane으로 인해 쪼개져야하는 상황이다 

 

mesh polygon이 삼각형이라고 한다면 쪼개지는 상황은 out 1 in 2 / out 2 in 1 두 가지 뿐이다

 

                const [vA, vB, vC] = [outside[0], inside[0], inside[1]];
                const tAB = vA.distance / (vA.distance - vB.distance);
                const tAC = vA.distance / (vA.distance - vC.distance);

                const vAB = vA.vertex.clone().lerp(vB.vertex, tAB);
                const vAC = vA.vertex.clone().lerp(vC.vertex, tAC);

일단 이건 out A와 in B C 사이에서 자르게 될 때 보간점을 찾는 로직이다

 

변수들의 규칙은

vAB는 A와 B사이의 보간된 점이고, tAB는 lerp 비율이다 

 

                newIndices.push(
                    getVertexIndex(vA.vertex),
                    getVertexIndex(vAB),
                    getVertexIndex(vAC)
                );
                newIndices.push(
                    getVertexIndex(vAB),
                    getVertexIndex(vB.vertex),
                    getVertexIndex(vC.vertex)
                );
                newIndices.push(
                    getVertexIndex(vAC),
                    getVertexIndex(vB.vertex),
                    getVertexIndex(vC.vertex)
                );

plane으로 자르게 되면 triangle mesh가 세 개 생기는 모양이다 

 

자른 면을 기준으로 위 모양처럼 세 개의 삼각형이 생기는 것 같은데

B와 C를 포함한 두 삼각형은 일부 겹친다 

일부로 이렇게 만들었나? 

 

            if (outside.length === 2 && inside.length === 1) {
                const [vA, vB, vC] = [inside[0], outside[0], outside[1]];
                const tAB = vA.distance / (vA.distance - vB.distance);
                const tAC = vA.distance / (vA.distance - vC.distance);

                const vAB = vA.vertex.clone().lerp(vB.vertex, tAB);
                const vAC = vA.vertex.clone().lerp(vC.vertex, tAC);

                newIndices.push(
                    getVertexIndex(vA.vertex),
                    getVertexIndex(vAB),
                    getVertexIndex(vAC)
                );
                newIndices.push(
                    getVertexIndex(vAB),
                    getVertexIndex(vB.vertex),
                    getVertexIndex(vC.vertex)
                );
            }

inside가 하나인 경우는 삼각형이 두 개만 생긴다 

 

이거다

아래쪽이 inside인데 잘려나가 새로운 메쉬가 생성되는 쪽의 퀄리티를 우선시 한 게 아닐까

 

    // 메쉬를 자르는 함수 호출
    init();
    cutMesh = cutMeshWithPlane(cube, new THREE.Plane(new THREE.Vector3(0, 1, 0), 0));
    scene.add(cutMesh);

마지막 코드를 보니 cube를 새로운 메쉬로 수정한다

즉 inside가 잘려나가는 쪽이 아니라 잘린 cube 중심쪽 mesh에 해당한다

 

그러면 ouside로 자르면 자른 부분은 그냥 없어지는 것 같다

그러면 inside 퀄리키가 우선이되는 이유를 알겠다

 

 

plane으로 자르는 로직을 보면 확실히 자르면서 잘리는 구간의 vertex를 새로 만들고 기존 mesh를 잘려서 수정된 mesh로 수정한다

 

위 코드에서는 다루지 않았는데 실제로는 잘려나간 mesh를 생성해주기까지 해야한다 

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Three.js Mesh Cutting</title>
    <style>
        body { margin: 0; }
        canvas { display: block; }
    </style>
</head>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
    let scene, camera, renderer, cube, planeMesh, cutMeshes;

    function init() {
        // 씬, 카메라, 렌더러 초기화
        scene = new THREE.Scene();
        camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
        renderer = new THREE.WebGLRenderer();
        renderer.setSize(window.innerWidth, window.innerHeight);
        document.body.appendChild(renderer.domElement);

        // 큐브 메쉬 생성 및 추가
        const geometry = new THREE.BoxGeometry();
        const material = new THREE.MeshBasicMaterial({ color: 0x00ff00, wireframe: true });
        cube = new THREE.Mesh(geometry, material);
        scene.add(cube);

        // 절단 평면 생성 및 추가
        const planeGeometry = new THREE.PlaneGeometry(5, 5);
        const planeMaterial = new THREE.MeshBasicMaterial({ color: 0xff0000, side: THREE.DoubleSide, wireframe: true });
        planeMesh = new THREE.Mesh(planeGeometry, planeMaterial);
        planeMesh.rotation.x = Math.PI / 2;  // 평면을 수평으로 회전
        scene.add(planeMesh);

        camera.position.z = 5;

        animate();
    }

    function animate() {
        requestAnimationFrame(animate);
        renderer.render(scene, camera);
    }

    // 메쉬 절단 함수
    function cutMeshWithPlane(mesh, plane) {
        const geometry = mesh.geometry.clone();
        geometry.computeVertexNormals();

        const positionAttribute = geometry.getAttribute('position');
        const newVertices = { positive: [], negative: [] };
        const newIndices = { positive: [], negative: [] };
        const vertexMap = { positive: new Map(), negative: new Map() };

        function getVertexIndex(v, side) {
            const key = `${v.x},${v.y},${v.z}`;
            if (!vertexMap[side].has(key)) {
                vertexMap[side].set(key, newVertices[side].length / 3);
                newVertices[side].push(v.x, v.y, v.z);
            }
            return vertexMap[side].get(key);
        }

        for (let i = 0; i < positionAttribute.count; i += 3) {
            const v0 = new THREE.Vector3().fromBufferAttribute(positionAttribute, i);
            const v1 = new THREE.Vector3().fromBufferAttribute(positionAttribute, i + 1);
            const v2 = new THREE.Vector3().fromBufferAttribute(positionAttribute, i + 2);

            const d0 = plane.distanceToPoint(v0);
            const d1 = plane.distanceToPoint(v1);
            const d2 = plane.distanceToPoint(v2);

            const outside = [];
            const inside = [];

            [[v0, d0], [v1, d1], [v2, d2]].forEach(([v, d]) => {
                if (d >= 0) {
                    outside.push({ vertex: v, distance: d });
                } else {
                    inside.push({ vertex: v, distance: d });
                }
            });

            if (inside.length === 0) {
                [v0, v1, v2].forEach(v => newIndices.positive.push(getVertexIndex(v, 'positive')));
                continue;
            }

            if (outside.length === 0) {
                [v0, v1, v2].forEach(v => newIndices.negative.push(getVertexIndex(v, 'negative')));
                continue;
            }

            const createIntersectionVertices = (vA, vB, vC) => {
                const tAB = vA.distance / (vA.distance - vB.distance);
                const tAC = vA.distance / (vA.distance - vC.distance);

                const vAB = vA.vertex.clone().lerp(vB.vertex, tAB);
                const vAC = vA.vertex.clone().lerp(vC.vertex, tAC);

                return { vAB, vAC };
            };

            if (outside.length === 1 && inside.length === 2) {
                const [vA, vB, vC] = [outside[0], inside[0], inside[1]];
                const { vAB, vAC } = createIntersectionVertices(vA, vB, vC);

                newIndices.positive.push(
                    getVertexIndex(vA.vertex, 'positive'),
                    getVertexIndex(vAB, 'positive'),
                    getVertexIndex(vAC, 'positive')
                );

                newIndices.negative.push(
                    getVertexIndex(vAB, 'negative'),
                    getVertexIndex(vB.vertex, 'negative'),
                    getVertexIndex(vC.vertex, 'negative'),
                    getVertexIndex(vAC, 'negative'),
                    getVertexIndex(vB.vertex, 'negative'),
                    getVertexIndex(vC.vertex, 'negative')
                );
            }

            if (outside.length === 2 && inside.length === 1) {
                const [vA, vB, vC] = [inside[0], outside[0], outside[1]];
                const { vAB, vAC } = createIntersectionVertices(vA, vB, vC);

                newIndices.positive.push(
                    getVertexIndex(vB.vertex, 'positive'),
                    getVertexIndex(vC.vertex, 'positive'),
                    getVertexIndex(vAB, 'positive'),
                    getVertexIndex(vC.vertex, 'positive'),
                    getVertexIndex(vAB, 'positive'),
                    getVertexIndex(vAC, 'positive')
                );

                newIndices.negative.push(
                    getVertexIndex(vA.vertex, 'negative'),
                    getVertexIndex(vAB, 'negative'),
                    getVertexIndex(vAC, 'negative')
                );
            }
        }

        const newGeometries = {
            positive: new THREE.BufferGeometry(),
            negative: new THREE.BufferGeometry()
        };

        newGeometries.positive.setAttribute('position', new THREE.Float32BufferAttribute(newVertices.positive, 3));
        newGeometries.positive.setIndex(newIndices.positive);

        newGeometries.negative.setAttribute('position', new THREE.Float32BufferAttribute(newVertices.negative, 3));
        newGeometries.negative.setIndex(newIndices.negative);

        const positiveMesh = new THREE.Mesh(newGeometries.positive, new THREE.MeshBasicMaterial({ color: 0x0000ff, wireframe: true }));
        const negativeMesh = new THREE.Mesh(newGeometries.negative, new THREE.MeshBasicMaterial({ color: 0xff00ff, wireframe: true }));

        return [positiveMesh, negativeMesh];
    }

    // 초기화 함수 호출 및 메쉬 절단 함수 호출
    init();
    cutMeshes = cutMeshWithPlane(cube, new THREE.Plane(new THREE.Vector3(0, 1, 0), 0));
    cutMeshes.forEach(mesh => scene.add(mesh));

</script>
</body>
</html>

코드를 생성해보면 이와 같다

 

cloth tailor를 만들기 위해 필요한 것

결국 cloth tailor 프로젝트를 위해서는 geometry를 수정하고 새로운 mesh를 생성하는 기능이 필수 불가결 할 것 같다 

 

당장 CLO와 같은 의상 제작 엔진을 보면 천을 새로 만들고 붙이고 한다 

 

그래서 생각나는 아이디어는 현재 시뮬레이션을 돌려본 원피스 모델처럼

기존에 있는 모델을 다루는 것도 좋지만

 

carment 씨의 프로젝트에서 이미 포함되어있던 천 오브젝트를 이용하는게 어떨까

잘라서 잘린 부분의 geometry를 가지고 새로운 mesh를 만드는 과정으로 천을 분리하고

또 천을 붙이는 기능을 만드는게 좋아보이지 싶다

 

새로운 모델을 찾으면 또 모델이 규격에 안맞는 경우에 찾아서 고쳐야하니 

그런 방면에서도 위와 같이 진행 하는 게 좋을 것 같다 

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