2D 타일 게임에서 빛 전파 구현하기 - Point Light

2020-01-15

Main 가벼운 빛 전파를 구현해봅시다

목차

  1. 2D 타일 게임에서 빛 전파 구현하기 - Point Light
  2. 2D 타일 게임에서 빛 전파 구현하기 - Directional Light & Color Light
  3. 2D 타일 게임에서 부드러운 물 구현하기

2D 타일 게임에서의 빛 전파

Example of 2D Light Propagation 대표적인 예시 : Terraria

다른 여러 게임에서 사용하는 동적 조명(엔진에서 제공하는)을 사용하면 조금 간단하게 빛을 표현할 수 있습니다. 하지만 동적 조명에는 여러 문제가 있는데, 특히 가장 치명적인 문제는 빛이 구석으로 퍼지지 않고, 조명이 많으면 많을수록 게임이 점점 느려지게 됩니다. 하지만 2D, 3D에 상관 없이 격자 기반의 게임에서는 각 셀을 샘플링하여 빛의 정도를 계산할 수 있습니다. 구현에 있어 크게 다르진 않지만, 점광원(Point Light)방향광원(Directional Light)를 구분하여 설명하겠습니다.

Point Light

Ambient Occlusion Chart 중심에서 멀어질수록 빛이 약해진다

우선 점광원부터 살펴봅시다. 플레이어가 빛의 세기가 255인 횃불을 설치하고, 이 빛은 주위로 퍼질수록 10만큼 감쇠된다고 합시다. 횃불의 있는 타일은 빛의 세기가 255이고, 타일의 주위는 245가 됩니다. 다시 각 이웃 타일을 기준으로 또 10만큼 줄인 235의 빛을 전파합니다. 나이브한 구현은 모든 타일을 순회하면서 세기가 0이 될 때까지 반복하는 것입니다. 하지만 이는 중복된 방문이 너무 많습니다. 광원의 영향을 받는 주위 타일만 반복하면 좋겠죠. 따라서 너비 우선 탐색(BFS)이 필요합니다.

빛의 전파

알고리즘의 2D 시각화 예시

플레이어가 설치한 횃불을 큐에 담습니다.

torchLightPropagationQueue.Enqueue(worldTilePosition);

큐가 비워질 때 까지 반복합니다.

while (torchLightPropagationQueue.Count != 0)

큐에서 가장 앞 노드를 꺼내옵니다.

Vector2Int lightPosition = torchLightPropagationQueue.Dequeue();
int torchLight = GetTorchLight(lightPosition);

if (torchLight <= 0)
    continue;

현재 노드의 이웃 타일을 순회하면서 빛을 전파합니다.

Vector2Int neighborPosition = lightPosition + direction; // direction : 주변 타일 방향 ex) Up : (x : 0, y : 1)

if (!TileUtil.BoundaryCheck(neighborPosition, mapSize)) // 맵의 경계 확인
    continue;

int neighborTorchLight = GetTorchLight(neighborPosition); // 이웃 점광원의 세기 

int resultTorchLight = torchLight - TileLight.TorchLightAttenuation; // 빛의 감쇠

bool isOpacity = GetTile(neighborPosition, out Tile neighborTile) && neighborTile.id != 0; // 이웃 타일이 빈 타일이 아니라면

if (isOpacity)
    resultTorchLight -= neighborTile.attenuation; // Solid 타일의 추가 감쇠

if (neighborTorchLight >= resultTorchLight) // 빛의 전파는 이웃보다 강도가 높을 때 한다
    continue;

SetTorchLight(neighborPosition, resultTorchLight); // 이웃 빛 Update
torchLightPropagationQueue.Enqueue(neighborPosition);

빛의 제거

이번엔 플레이어가 설치한 횃불을 다시 제거하는 상황을 생각해봅시다. 물론 제거한 횃불을 제외하고 맵의 모든 횃불을 큐에 새로 담아서 BFS를 돌리면 되지만, 이는 역시 불필요한 계산이 많습니다. 이때는 빛을 전파하는 과정을 조금 변형한 알고리즘을 사용합니다.

플레이어가 제거한 횃불을 큐에 담습니다. 제거할 빛의 세기를 같이 기억합니다.

torchLightRemovalQueue.Enqueue(new Tuple<Vector2Int, int>(worldTilePosition, neighborLight));

큐가 비워질 때 까지 반복합니다.

while (removalQueue.Count != 0)

큐에서 가장 앞 노드를 꺼내옵니다.

(Vector2Int lightPosition, int torchLight) = removalQueue.Dequeue();

현재 노드의 이웃 타일을 순회하면서 빛을 제거합니다.

Vector2Int neighborPosition = lightPosition + direction; // 주변 타일의 위치

if (!TileUtil.BoundaryCheck(neighborPosition, mapSize)) // 맵의 경계 확인
    continue;

int neighborTorchLight = GetLight(neighborPosition); // 이웃 점광원의 세기

// 빛을 삭제할 때는 똑같이 이웃보다 강도가 높을 때 한다
if (neighborTorchLight != 0 && neighborTorchLight < torchLight)
{
    SetLight(neighborPosition, 0); // 빛을 0으로 설정한다
    removalQueue.Enqueue(new Tuple<Vector2Int, int>(neighborPosition, neighborTorchLight));
}
else if (neighborTorchLight >= torchLight) // 만약 이웃이 더 강도가 세다면 이웃의 빛을 전파한다
{
    propagationQueue.Enqueue(neighborPosition);
}

결론

이번 글에서는 점광원의 전파를 다뤄보았습니다. 다음 파트에서는 방향광(햇빛)과 부드러운 색상광을 구현해보겠습니다.

유용한 링크

코드 (Git Repository)