새로운 모드 만들기 : 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를 만드는 과정으로 천을 분리하고
또 천을 붙이는 기능을 만드는게 좋아보이지 싶다
새로운 모델을 찾으면 또 모델이 규격에 안맞는 경우에 찾아서 고쳐야하니
그런 방면에서도 위와 같이 진행 하는 게 좋을 것 같다