오진수의 작업실
 · 프로그래밍

CAD 프로그램 곡면 벽 구현하기

Segment + Arc를 Edge로 가지는 Polygon 활용법

시연 영상입니다. 곡면 벽이 그려지고, 방이 자동으로 나눠집니다. 선택했을 때 방도 경계에 맞게 둥글게 외곽선이 그려집니다.

스케치업과 연동해서 절차적으로 곡면 벽을 생성했습니다. 곡면이 들어간 것만으로도 꽤 아름다운 인테리어가 탄생합니다.


곡면 벽을 추가하면 단순히 보여지는 부분만 바뀌는 게 아닙니다. 클래스 정의가 바뀌고, 클래스가 다른 클래스와 상호작용하는 코드가 바뀝니다.


보시다시피 인테리어 설계 프로그램은 다각형을 많이 사용합니다. 그런데 '곡면'이라는 개념이 추가되면서 다각형 정의를 바꿔야 했습니다. 어떤 상태에서 어떻게 바뀌었는지 살펴보겠습니다.

꼭짓점으로 정의되는 다각형

곡면 벽 기능을 추가하기 전까지는 오로지 벽을 선분(Segment) 형태로 정의했습니다. 선분 형태는 좌표계 위의 두 점에 의해 정의됩니다. 벽은 두께가 있는 선분입니다. 따라서 두 점과 함께 두께를 속성으로 가지게 되죠.


class Wall {
  ps: Point
  pe: Point
  thickness: Point
}


그렇다면 이 벽에 의해 나뉘는 방 모양은 어떻게 정의할 수 있을까요? 꼭짓점 배열을 가지는 다각형으로 정의할 수 있습니다.


class Room {
  shape: Polygon
}

class Polygon {
  vertices: Point[]
}


점과 점 사이를 선분으로 사용하기로 합의한다면 이러한 방식이 가능합니다. 하지만 문제는 앞서 보여드린 동영상 같은 예시입니다. 점과 점 사이가 선분이 아니라 곡선이 올 수도 있는 겁니다.


그렇기 때문에 일단 꼭짓점 배열을 이용한 정의는 접어두었습니다. 꼭짓점을 연결하는 선이 직선인지 곡선인지 표현할 방법이 제한적이기 때문입니다.

모서리로 정의되는 다각형

이때는 모서리에 의해 다각형을 정의해야 합니다. 모서리는 선분(Segment)일 수도, 원호(Arc)일 수도 있다고 합시다. 선분일 때는 원래처럼 두 점을 이용해 정의하면 됩니다. 원호일 때는 중심점, 반지름, 시작 각도, 끝 각도 그리고 저는 회전 방향까지 이용해 정의했습니다.


// segment.ts
class Segment {
  ps: Point
  pe: Point
}


// arc.ts
class Arc {
  center: Point
  radius: number
  startAngle: number
  endAngle: number
  counterClockwise: boolean

  get ps(): Point
  get pe(): Point
}


편의상 Arc 클래스에는 각각 원호의 시작점과 끝점을 반환하는 함수 ps(), pe()를 넣어주었습니다.


이제 다각형은 이렇게 정의할 수 있습니다.


// polygon.ts
import type { Segment } from "segment.ts"
import type { Arc } from "arc.ts"

type Edge = Segment | Arc

class Polygon {
  edges: Edge[]
}

방 나누기 알고리즘

방을 나누려면 개발자 입장에서는 방을 찾아야 합니다. 제가 사용했던 방 찾기 알고리즘은 단순합니다. 어떤 선분의 꼭짓점에서 연결된 다른 선분의 꼭짓점으로 이동하면서 닫힌 순환 구조를 찾아내는 방식입니다. 이동하다가 그동안 지나온 꼭짓점 중 하나로 돌아온다면 그 순환하는 꼭짓점 배열이 곧 방 모양을 나타내는 다각형이겠죠.


const segments = getWallsShapeBoundarySegments(walls)

const splittedSegments = splitSegmentsByIntersections(segments)

const rooms: Polygon[] = []

const vertices = getUniqueVertices(splittedSegments)

for (const vertex of vertices) {
  const room: Point[] = []

  let currentVertex = vertex

  while {
    const connectedVertex = findConnectedVertices(currentVertex)

    if (!connectedVertex) {
      break
    }

    if (room.length >= 3 && connectedVertex === room[0]) {
      rooms.push(new Polygon(room))

      break
    }

    room.push(connectedVertex)

    currentVertex = connectedVertex
  }
}


이런 알고리즘은 다각형이 모서리로 정의되었다고 해서 크게 달라지지는 않습니다. 다만 이동중에 꼭짓점만 남기지 말고 선분인지 원호인지를 알 수 있는 정보도 함께 남겨야 합니다. 제일 좋은 방법은 참조 객체를 그대로 갖고 있는 것입니다.


const edges = getWallsShapeBoundaryEdges(walls)

const splittedEdges = splitEdgesByIntersections(segments)

const rooms: Edge[][] = []

const vertices = getUniqueVertices(splittedSegments)

for (const vertex of vertices) {
  const room: Edge[] = []

  let currentVertex = vertex

  while {
    type ConnectedEdge = {
      edge: Edge
      from: Point
      to: Point
    }

    const connectedEdge: ConnectedEdge = findConnectedEdge(currentVertex)

    if (!connectedEdge) {
      break
    }

    if (room.length >= 3 && connectedEdge.to === room[0].from) {
      rooms.push(new Polygon(room))

      break
    }

    room.push(connectedEdge.edge.copyWith(connectedEdge.edge.from, connectedEdge.edge.to))

    currentVertex = connectedEdge.to
  }
}


방을 찾을 때는 기존 선분의 방향이 달라질 수 있으니 그 부분만 유의하면 됩니다.

나중에는 Bezier Curve로 정의되는 Polygon도...

실제 케이스를 바탕으로 Segment와 Arc에 의해 정의되는 Polygon을 알아보았습니다. 프로그램을 개발할 때 처음부터 Segment와 Arc에 의해 Polygon을 정의해 본다면 어떨까요? 저와 비슷한 프로그램을 만들고 계신다면 도움이 될 수 있을 것 같습니다.


개발 효율성을 고려하여 아직까지는 Segment + Arc 조합에서 그쳤습니다. 하지만 나중에는 Arc를 넘어서 Bezier Curve로 정의되는 Polygon도 만들어 보고 싶습니다. 하이엔드 인테리어를 설계하는 경우에 자유 곡선을 꽤 많이 사용하기 때문입니다. 일단 오늘은 이만 마치겠습니다.

프로필 이미지

댓글

프로그래밍 카테고리 다른 글

  1. Flutter 상태관리 Provider면 충분하다Flutter 상태관리 Provider면 충분하다
     · 프로그래밍 · 댓글 1