실제로 그려보기 (유니티 c# 사용)
물론 직접 그리진 않고 라이브러리의 도움을 받을것입니다.
저는 csDelaunay라는 라이브러리를 사용하려고 하는데,
쓰다 보니 불편한 점이 이만저만이 아니라 저장소를 포크하고 제 입맛대로 마개조했습니다.
이 저장소의 master
브랜치를 포크해주시면 되겠습니다.
아직 연재중인 글이기 때문에 아예 포크하셔서 앞으로 있을지 모르는 변경점을 업데이트 받으시는 것을 추천드립니다.
우선, 보로노이 다이어그램을 생성 후 시각화해봅시다.
보로노이 다이어그램을 이루는 다각형의 무게중심을 랜덤하게 생성해야합니다.
[SerializeField]
private Vector2Int size;
[SerializeField]
private int nodeAmount = 0;
우선 Map이라는 클래스를 만들고 두 필드를 선언해줍시다.
size
필드는 시각화를 위한 텍스쳐의 크기이고,
nodeAmount
필드는 무게중심의 수입니다.
인스펙터에서 수정 가능하도록 이런 식으로 선언해주세요.
private Voronoi GenerateVoronoi(Vector2Int size, int nodeAmount)
{
var centroids = new List<Vector2>();
// 무게 중심을 nodeAmount만큼 생성
for (var i = 0; i < nodeAmount; ++i)
{
var x = Random.Range(0, size.x);
var y = Random.Range(0, size.y);
centroids.Add(new Vector2(x, y));
}
var Rect = new Rect(0f, 0f, size.x, size.y);
var voronoi = new Voronoi(centroids, Rect);
return voronoi;
}
랜덤한 좌표에 무게중심을 생성한 후,Voronoi
인스턴스 생성자에 생성한 무게중심과 범위를 나타내는 Rect
를 파라미터로써 넣어줍시다.
이러면 알아서 잘 보로노이 다이어그램을 생성해줍니다.
Voronoi
클래스는 csDelaunay
라이브러리의 클래스이므로,
스크립트에 using csDelaunay;
라인을 꼭 추가해주세요.
그럼 이제 시각화해봅시다.
먼저 생성된 무게중심이라도 찍어볼까요?
우선 저는 보로노이 다이어그램을 시각화하기 위한 함수들을 모아둔 클래스를
적당히 MapDrawer라는 이름으로 팠습니다.
이 클래스는 굳이 유니티에서 컴포넌트로써 사용하지 않고 전역함수들만 모아둘거라
Monobehaviour 상속은 빼주셔도 됩니다.
public static Sprite DrawVoronoiToSprite(Voronoi voronoi)
{
// 텍스쳐 픽셀 하나하나의 색을 담고있는 배열입니다.
// 애석하게도, Texture2D의 픽셀 정보는 1차원 배열입니다.
var rect = voronoi.PlotBounds;
var width = Mathf.RoundToInt(rect.width);
var height = Mathf.RoundToInt(rect.height);
var pixelColors = Enumerable.Repeat(Color.white, width * height).ToArray();
var siteCoords = voronoi.SiteCoords();
// 무게중심 그리기
foreach (var coord in siteCoords)
{
var x = Mathf.RoundToInt(coord.x);
var y = Mathf.RoundToInt(coord.y);
var index = x * width + y;
pixelColors[index] = Color.red;
}
// 모서리 그리기
// . . .
// 텍스쳐화 시키고 스프라이트로 만들기
var size = new Vector2Int(width, height);
return DrawSprite(size, pixelColors);
}
public static Sprite DrawSprite(Vector2Int size, Color[] colorDatas)
{
var texture = new Texture2D(size.x, size.y);
texture.filterMode = FilterMode.Point;
texture.SetPixels(colorDatas);
texture.Apply();
var rect = new Rect(0, 0, size.x, size.y);
var sprite = Sprite.Create(texture, rect, Vector2.one * 0.5f);
return sprite;
}
인스펙터에서 설정한 size
만큼 텍스쳐에 입힐 픽셀 컬러를 계산한 후
Texture2D
를 생성해 계산한 픽셀 컬러를 넣어줍니다.
그 후 생성했던 텍스쳐를 토대로 스프라이트를 생성합니다.
[SerializeField]
private Vector2Int size;
[SerializeField]
private int nodeAmount = 0;
[SerializeField]
private SpriteRenderer voronoiMapRenderer = null;
private void Awake()
{
var voronoi = GenerateVoronoi(size, nodeAmount);
voronoiMapRenderer.sprite = MapDrawer.DrawVoronoiToSprite(voronoi);
}
다시 Map 클래스로 돌아가 인스펙터에서 적당히 size
와 nodeAmount
값을 정하고,
voronoiMapRenderer 필드를 SpriteRenderer가 달린 게임오브젝트를 새로 생성해
인스펙터에서 대입시켜준 후 돌려봅시다.
이젠 보로노이 다이어그램의 모서리를 시각화해봅시다.
무게중심 그리는 부분의 밑 라인에 "모서리 그리기"라는 주석을 남겨뒀던 곳에 이 코드가 들어갑니다.
// 모서리 그리기
// 먼저 모든 폴리곤의 정보를 얻어온다.
foreach (var site in voronoi.Sites)
{
// 그 폴리곤의 모든 이웃 폴리곤을 얻어온다.
var neighbors = site.NeighborSites();
foreach (var neighbor in neighbors)
{
// 이웃한 폴리곤들에게서 겹치는 가장자리(edge)를 유도해낸다.
var edge = voronoi.FindEdgeFromAdjacentPolygons(site, neighbor);
// 뭔진 밑에서 설명해드리겠습니다.
if (edge.ClippedVertices is null)
{
continue;
}
// 가장자리를 이루는 모서리 정점(vertex) 2개를 얻어온다.
var corner1 = edge.ClippedVertices[LR.LEFT];
var corner2 = edge.ClippedVertices[LR.RIGHT];
// 1차 함수의 그래프를 그리듯이 텍스쳐에 가장자리 선분을 그린다.
var targetPoint = corner1;
var delta = 1 / (corner2 - corner1).magnitude;
var lerpRatio = 0f;
while ((int)targetPoint.x != (int)corner2.x ||
(int)targetPoint.y != (int)corner2.y)
{
// 선형 보간을 통해 corner1과 corner2 사이를 lerpRatio만큼 나누는 점을 얻어온다.
targetPoint = Vector2.Lerp(corner1, corner2, lerpRatio);
lerpRatio += delta;
// 텍스쳐의 좌표 영역은 (0 ~ size.x - 1)이지만,
// 생성한 보로노이 다이어그램의 좌표 영역은 (0 ~ (float) size.x)이다.
var x = Mathf.Clamp((int) targetPoint.x, 0, size.x - 1);
var y = Mathf.Clamp((int) targetPoint.y, 0, size.y - 1);
var index = x * size.x + y;
pixelColors[index] = Color.black;
}
}
}
단번에 이해하기 힘든 코드지만, 천천히 설명해드리겠습니다.
그러나 내용을 이해하시려면 적어도 1편의 맨 밑에 있는 요약 정도는 읽어주세요.
먼저, 모든 가장자리의 정보를 얻기 위해 모든 폴리곤의 정보를 얻어와야 합니다.
인접한 폴리곤 2개끼리 서로 겹치는 1개의 가장자리를 얻어낼 수 있기 때문입니다.
각각의 폴리곤의 모든 인접 폴리곤 (이웃 폴리곤)을 얻어, 해당 폴리곤과 모든 인접 폴리곤 사이에서 가장자리를 유도해낼 수 있습니다. 이를 모든 폴리곤에 대해 수행한다면 모든 가장자리의 정보를 얻을 수 있을 것입니다.
가장자리의 정보를 얻은 후 유의해야 할 점은, 동그라미로 표시한 곳 처럼 보로노이 다이어그램의 구석에 있는 폴리곤의 가장자리 일부는 무한히 뻗는다는 점입니다. 그렇기 때문에 ClippedVertices
프로퍼티를 사용해주세요.
이 프로퍼티는 무한히 뻗은 가장자리를 사각형 범위로 제한한 것입니다.
이 프로퍼티가 null인 경우엔 폴리곤의 가장자리를 이루는 정점이 사각형 범위 바깥에 있다는 의미기 때문에,
해당 가장자리에 대한 선 긋는 공정은 건너뛰셔야 합니다.
그 후 1차 함수의 그래프를 그리듯이 텍스쳐에 선을 그려주시면 됩니다. (참고 자료)
어찌저찌 시각화를 해봤지만 모양이 좀 불-편합니다.
폴리곤의 모양과 크기의 편차가 심하기 때문입니다.
이럴 때 조치할 수 있는 방법이 있습니다.
Lloyd's Relaxation(로이드의 알고리즘) 이라는 작업을 반복해서 돌리면 됩니다.
다행스럽게도, 이미 csDelaunay 라이브러리에서 이 작업을 지원합니다.
이 작업은 모든 폴리곤의 무게중심을 인자로 사용해 보로노이 다이어그램을 새로 생성하는데,
이 작업을 반복할수록 폴리곤의 모양과 크기의 편차가 줄어듭니다.
적당히 5번 돌려보겠습니다.
Map 클래스의 GenerateVoronoi 함수로 돌아가 파라미터 타입을 바꾸고,
호출하는 곳으로 돌아가 파라미터를 추가해줍시다.
// 필드 새로 선언
[SerializeField]
private int lloydIterationCount = 0;
// 파라미터 추가
private Voronoi GenerateVoronoi(Vector2Int size, int nodeAmount, int lloydIterationCount)
// 구현부 내부의 인스턴스 생성자에 파라미터 추가
var voronoi = new Voronoi(centroids, Rect, lloydIterationCount);
// GenerateVoronoi 호출부에 파라미터 추가
var voronoi = GenerateVoronoi(size, nodeAmount, lloydIterationCount);
로이드의 알고리즘 반복 횟수를 나타내는 인자를 하나 추가합시다.
인스펙터에서 반복 횟수를 조절하고 싶으시다면 알아서 클래스 필드로 빼시면 되겠습니다.
변화가 아주 드라마틱하네요... 이정도는 되어야 모양이 좀 이쁘장하다고 말할 수 있을 것 같습니다 ㅎㅎ
'이론' 카테고리의 다른 글
동적으로 섬 생성하기 - 4 (지형 생성 해보기) (0) | 2021.03.27 |
---|---|
동적으로 섬 생성하기 - 3 (노이즈를 사용한 지형 생성) (0) | 2021.03.26 |
동적으로 섬 생성하기 - 1 (바이옴 나누기) (0) | 2021.03.25 |