wan의 개발일기

RoomCraft #14. 방을 다각형으로 - 사각형을 넘어서기 위한 첫걸음 본문

1인 게임개발 활동/가구배치 시뮬레이터

RoomCraft #14. 방을 다각형으로 - 사각형을 넘어서기 위한 첫걸음

wan_ 2026. 7. 5. 18:09
소스코드 → github.com/Jywan/RoomCraft

들어가며

지난 편에서 버그 두 개 잡고 가구 정보 패널을 통합했다. 이번엔 로드맵 순서를 좀 바꿨다 — 원래 다음 순서는 WebGL/모바일 배포였는데, 아직 최적화도 안 끝난 상태에서 배포부터 준비하는 게 이상해서 그건 맨 뒤로 미루고, 대신 L자형 방 같은 다양한 방 모양 지원을 먼저 하기로 했다.

근데 이게 생각보다 큰 작업이라, 세 단계로 나눴다.

  1. 다각형 기반 방 생성 (이번 편) — 바닥/벽을 사각형 전용이 아니라 어떤 모양이든 처리할 수 있게 구조 변경
  2. 프리셋 모양 선택 — 사각형/L자형 같은 미리 정의된 모양을 UI에서 고르기
  3. 커스텀 편집 — 사용자가 직접 꼭짓점을 찍어서 모양 만들기

이번 편은 1번만 다룬다. 겉으로는 사각형 방이 예전이랑 똑같이 나오는데, 속은 완전히 갈아엎었다.


사각형 전용 구조의 한계

원래 RoomData는 가로/세로만 갖고 있고, RoomBuilder는 그 값으로 바닥 큐브 하나 + 벽 4개를 축에 맞춰 딱 찍어내는 구조였다. L자형이나 커스텀 모양을 넣으려면 이 자체를 바꿔야 했다 — "사각형"도 "L자형"도 결국 꼭짓점 리스트 하나 던져주면 바닥+벽을 만들어주는 함수 위에서 처리하는 게 맞다.

RoomData — 꼭짓점 리스트 추가

기존 저장 파일이랑 호환되게, vertices가 없으면 width/depth로 사각형을 자동 생성하는 GetVertices()를 만들었다.

public List<Vector2> GetVertices()
{
    if (vertices != null && vertices.Count >= 3)
        return vertices;

    float halfW = width / 2f;
    float halfD = depth / 2f;
    return new List<Vector2>
    {
        new Vector2(-halfW, -halfD),
        new Vector2(halfW, -halfD),
        new Vector2(halfW, halfD),
        new Vector2(-halfW, halfD)
    };
}

Ear Clipping — 다각형을 삼각형으로 쪼개기

바닥을 렌더링하려면 다각형을 삼각형들로 쪼개야 한다(GPU는 삼각형만 그릴 수 있으니까). 사각형이야 대각선 하나 그으면 끝이지만, L자형 같은 오목한 모양은 그렇게 간단하지 않다. Ear Clipping이라는 표준 알고리즘을 직접 구현했다 — 원리는 "다른 꼭짓점을 포함하지 않는 볼록한 삼각형(귀, ear)을 하나씩 잘라내면서 다각형을 줄여나간다"는 것.

public static List<int> Triangulate(List<Vector2> polygon)
{
    List<int> indices = new List<int>();
    List<Vector2> points = new List<Vector2>(polygon);

    // Ear Clipping은 반시계방향(CCW) 기준이라, 시계방향이면 뒤집는다
    if (SignedArea(points) < 0f)
        points.Reverse();

    List<int> remaining = new List<int>();
    for (int i = 0; i < points.Count; i++)
        remaining.Add(i);

    while (remaining.Count > 3)
    {
        // 볼록하고, 다른 점을 안 품고 있는 삼각형(귀)을 찾아서 잘라낸다
        // (본문 생략, 전체 코드는 PolygonUtils.cs 참고)
    }

    return indices;
}
삼각형 판정에 외적(cross product)을 두 군데 쓴다 — 세 점이 볼록한지 확인할 때, 그리고 어떤 점이 삼각형 안에 있는지 확인할 때. 둘 다 같은 Cross() 함수 하나로 처리했다.

RoomBuilder — 바닥은 메쉬, 벽은 변마다 하나씩

바닥은 삼각분할한 결과로 커스텀 Mesh를 만든다. 벽은 예전처럼 앞/뒤/좌/우 4개를 하드코딩하는 대신, 꼭짓점을 순서대로 돌면서 변(edge)마다 벽 하나씩 자동으로 생성한다.

private void CreateWalls(RoomData data, List<Vector2> outline)
{
    float wallThickness = 0.1f;
    for (int i = 0; i < outline.Count; i++)
    {
        Vector2 start = outline[i];
        Vector2 end = outline[(i + 1) % outline.Count];
        CreateWallSegment($"Wall_{i}", data, start, end, wallThickness);
    }
}

사각형이면 꼭짓점 4개라 벽 4개, L자형이면 꼭짓점 6개라 벽 6개가 자동으로 나온다. 벽 하나의 회전각은 시작점→끝점 방향 벡터로 계산한다.

float dx = end.x - start.x;
float dz = end.y - start.y;
float angle = Mathf.Atan2(-dz, dx) * Mathf.Rad2Deg;
Unity의 Y축 회전은 로컬 X축을 월드 (cosθ, 0, -sinθ)로 매핑한다. 벽의 "긴 방향"(로컬 X)을 변의 방향과 맞추려고 이 관계를 거꾸로 풀어서 각도를 구했다.

삽질 기록

엉뚱한 Vector2를 참조해서 난 컴파일 에러

PolygonUtils.cs 맨 위에 using System.Numerics;를 썼는데, 이러면 Vector2가 Unity 타입이 아니라 .NET의 System.Numerics.Vector2로 잡힌다. 그 타입은 필드가 X/Y(대문자)라서, 코드에 있던 .x/.y가 전부 존재하지 않는 멤버가 돼서 컴파일이 안 됐다.

해결: using UnityEngine;으로 수정. using문 하나 잘못 쓴 걸로 타입 전체가 바뀌어버리는 걸 직접 겪어봤다.

사각형이 나비 모양으로 꼬임

GetVertices()의 사각형 기본값을 만들 때 꼭짓점 순서를 잘못 넣어서, 테두리를 따라 도는 게 아니라 대각선으로 교차하는 형태가 됐다. Ear Clipping은 자기교차 없는 단순 다각형을 전제로 하는 알고리즘이라 이대로면 바닥이 이상하게 나온다.

해결: 꼭짓점을 테두리 순서(좌하→우하→우상→좌상)대로 나열하도록 수정.

바닥이 마젠타로 렌더링됨

컴파일 에러 다 잡고 실행해봤더니 바닥이 새빨간 마젠타색으로 나왔다. Unity에서 마젠타는 "머티리얼(셰이더)이 없다"는 에러 표시다.

screenshot
바닥이 마젠타로 렌더링된 모습 — Unity의 "머티리얼 없음" 표시

알고 보니 Floor Material/Wall Material이 애초에 할당돼 있지 않았다. 예전엔 GameObject.CreatePrimitive(PrimitiveType.Cube)로 바닥/벽을 만들어서, Unity가 자동으로 기본 머티리얼을 붙여줬던 것 — 그래서 머티리얼을 안 넣어도 티가 안 났다. 이번에 바닥을 커스텀 Mesh + AddComponent<MeshRenderer>() 방식으로 바꾸면서, 그 자동 할당이 없어져서 진짜 "머티리얼 없음" 상태가 그대로 드러난 거였다.

해결: URP Lit 셰이더로 머티리얼을 새로 만들어서 Floor Material/Wall Material에 연결. 벽도 사실 지금까지 "우연히" 기본 머티리얼로 굴러가고 있었던 거라, 이 참에 제대로 된 머티리얼로 교체했다.

바닥과 가구가 겹치는 곳에서 깜빡임

머티리얼까지 고치고 나니 이번엔 바닥 위에 가구를 올려둔 자리가 지지직거리며 깜빡였다. Z-파이팅이라는 현상인데, 바닥 메쉬와 가구 밑면이 둘 다 Y=0에 정확히 겹쳐서 렌더러가 어느 게 앞인지 매 프레임 헷갈려하며 생기는 깜빡임이다. 예전 큐브 바닥은 두께(0.1m)가 있어서 잘 안 드러났는데, 지금은 두께 없는 평면이라 바로 티가 났다.

해결: 바닥을 Y = -0.01로 살짝 내려서 가구 밑면이랑 안 겹치게 했다.

정리

기능핵심 API / 구조
꼭짓점 기반 방 데이터RoomData.vertices + GetVertices() (하위호환용 사각형 fallback)
다각형 삼각분할PolygonUtils.Triangulate() — Ear Clipping
바닥 메쉬 생성Mesh + MeshFilter/MeshRenderer/MeshCollider
벽 자동 생성꼭짓점 순회하며 변마다 CreateWallSegment() 호출, Atan2로 회전각 계산
마젠타 버그 수정URP Lit 머티리얼 생성 후 Floor/Wall Material 연결
Z-파이팅 수정바닥 Y 위치를 -0.01로 오프셋