TECHARTNOMAD | TECHARTFLOWIO.COM

TECH.ART.FLOW.IO

[번역]GPU Run-time Procedural Placement on Terrain

jplee 2024. 2. 5. 12:48

엮인 글 함께 보기.
https://techartnomad.tistory.com/153

[번역]Efficient GPU Rendering for Dynamic Instances in Game Development

역자의 말. 아트디렉터 경험부터 테크니컬 아티스트 경험까지 다양한 경험을 갖고 있는 Kacper Szwajka 의 흥미롭고 실험적인 두 개의 토픽을 소개 하려고 합니다. 원문. Efficient GPU Rendering for Dynamic I

techartnomad.tistory.com

 
역자의 말.
절차적 모델링이나 절차적 배치 등등의 기법들은 2015년 제가 큰 관심을 갖을 때에는 업계에서 막 개념만 태동했던 거라면 이제는 실제 제작에 많이 도입 되고 있는 형태 입니다. 생각해 보면 8년이라는 시간이 넘게 지났군요. 이러한 제작 방식이 실무에 적용 되기까지 참 오래도 걸리는 것 같습니다. 이렇게 보면 게임개발진영은 꽤나 보수적인 듯 합니다. Kacper Szwajka 의 절차적 Placement 관련 토픽을 간략히 번역 해 봤습니다. 이미 UE5 에는 자체적인 기능이 통합 되었지만 유니티에서는 외부 플러그인등의 힘들 빌어서 작업하거나 직접 구현 해야합니다. 참고해서 구현 해 보는 것도 좋겠네요.


원문

GPU Run-time Procedural Placement on Terrain

In this article, I will explain the method for implementing run-time GPU placement of objects on terrain, using Unity engine. This…

medium.com

이 글에서는 Unity 엔진을 사용하여 지형에 오브젝트를 런타임 GPU로 배치하는 방법을 설명합니다. 이 기술은 컴퓨트 셰이더를 지원하는 모든 엔진에 적용할 수 있습니다.

문제: 수 평방 킬로미터에 이르는 넓은 지형에 오브젝트를 수동으로 배치하는 것은 게임 개발 과정에서 어려운 작업이 될 수 있습니다. 잦은 반복과 조정이 필요한 경우 특히 더 어렵습니다. 또한 메모리 사용량도 고려해야 합니다. 2x2km 맵을 0.5m 밀도의 잔디로 채우면 1,600만 개의 인스턴스가 발생하며, 변환 데이터를 저장하는 데만 약 1024MB가 필요합니다. 바로 이 지점에서 절차적 배치가 중요해집니다.
절차적 배치: 솔루션
프로시저럴 배치는 카메라 범위 내의 렌더링 인스턴스에만 메모리를 할당하고 카메라가 움직일 때 동적으로 인스턴스를 흩어지게 하는 솔루션을 제공합니다. 이 접근 방식은 게릴라 게임즈에서 "Horizon: 제로 던'에서 사용한 기법에서 영감을 얻었으며, 프로젝트의 특정 요구 사항에 맞게 조정했습니다.

프로세스 개요
이 프로세스에는 카메라 주변에 '청크'를 나타내는 평평한 그리드를 만드는 작업이 포함됩니다. 이 그리드의 각 셀, 즉 청크에는 오브젝트 인스턴스가 될 포인터가 포함됩니다. 카메라가 움직일 때 뒤에서 앞으로 전환되는 청크만 업데이트합니다. 그런 다음 이러한 '더티' 청크의 포인터를 재구성하고 각 포인터에 프로토타입을 할당합니다. 마지막 단계는 선택한 프로토타입 매개변수를 기반으로 포인터 트랜스폼을 설정하는 것입니다.

이 시스템의 로직은 대부분 컴퓨트 셰이더를 사용하여 실행되며, 수많은 간접 디스패치를 통해 필요한 데이터만 업데이트되도록 합니다.
풋프린트 및 생태 유형
' 풋프린트 '과 '생태 유형'이 결합된 ' 풋프린트 ' 개념이 이 시스템의 핵심입니다. 풋프린트는 포인터 사이의 평균 거리를 정의하고 이러한 포인터를 배치할 때 알고리즘의 응답을 지시합니다. 또한 특정 배치 규칙을 가진 사용 가능한 프로토타입의 그룹인 에코타입 목록도 포함됩니다.

청크
청크 로직은 주로 컴퓨팅 셰이더를 통해 처리됩니다. 각 청크는 대략 512개의 포인터를 저장합니다. 이 숫자가 청크당 계산에 가장 효율적이라는 것을 발견했습니다. 이러한 청크의 그리드 크기는 에코타입으로부터의 최대 세부 수준(LOD) 거리에 따라 달라집니다. 다음 그림에서 볼 수 있듯이 한 청크가 다른 청크를 대체할 때 눈에 띄는 '팝핑'을 방지하는 것이 중요합니다:

Offseting chunks. Notice how chunks indexes change.
[numthreads(GPUI_THREADS, 1, 1)]
void ChunkPositionUpdate (uint3 id : SV_DispatchThreadID) {
    if (id.x >= maxInstances) return;

    Chunk chunk = chunks[id.x];
    int2 spatial = chunk.spatialPos;

    if (spatial.x < newGridMin.x || spatial.x > newGridMax.x || spatial.y < newGridMin.y || spatial.y > newGridMax.y) {
        chunk.spatialPos = newGridMax - (chunk.spatialPos - oldGridMin);
        chunk.BBPosition = float3(chunk.spatialPos.x * chunkSize, 0, chunk.spatialPos.y * chunkSize) + (float)(chunkSize) * 0.5f;

        chunks[id.x] = chunk;
        chunksAppend.Append(chunk); // Mark as dirty for indirect dispatch
    }
}

포인터
포인터는 미래의 프로토타입 위치를 나타냅니다. 각 청크에는 동일한 수의 포인터가 있으며 청크 인덱스에 따라 포인터 위치를 계산합니다.

실제 스케일 청크 그리드 + 청크 포인터(프로토타입 밀도 파라미터가 다르기 때문에 모든 포인터가 표시되지는 않음)

다음은 포인터를 분산시키는 방법을 보여주는 예제 코드입니다. _SquareGrid  또는 _VoronoiGrid  는 포인터 분산에 대한 다른 설정 응답입니다.

float2 min = chunk.BBPosition.xz-chunk.BBSize.xz*0.5f;
float2 max = chunk.BBPosition.xz+chunk.BBSize.xz*0.5f;

float2 pos = 0;
#if _SquareGrid
  pos = S_GridPos(id.x,uint2(gridSize,gridSize),min,max);
#elif _VoronoiGrid
  pos = VoronoiPos(id.x,min,max);
#endif

// Apply jitter
uint rng = S_PosToHash(pos);
float angle = S_Random(rng)*K_TWO_PI;
float r = S_Random(rng);
float2 offset = float2(sin(angle),cos(angle))*jitter*r;
pos += offset;

난수
난수 생성은 포인터의 자연스러운 분포를 위해 매우 중요합니다. S_PosToHash() 함수는 주로 GPU Gems 3에 있는 "CUDA를 이용한 효율적인 난수 생성 및 적용"의 기술을 기반으로 합니다. 다음은 해싱 함수의 구현 예제입니다:

#define TAUS_STEP_1         ((z1 & 4294967294U) << 12) ^ (((z1 << 13) ^ z1) >> 19)
#define TAUS_STEP_2         ((z2 & 4294967288U) << 4) ^ (((z2 << 2) ^ z2) >> 25)
#define HYBRID_TAUS_N2      ((z1 ^ z2) & 268435455)

uint S_Hash(uint s) 
{
    s ^= 2747636419u;
    s *= 2654435769u;
    s ^= s >> 16;
    s *= 2654435769u;
    s ^= s >> 16;
    s *= 2654435769u;
    return s;
}
float S_Random(in out uint rngState) // 0-1
{
    rngState = S_Hash(rngState);
    return float(rngState) / 4294967295.0; // 2^32-1
}
uint S_PosToHash(float2 pos)
{
    uint z1 = asuint(pos.x);
    uint z2 = asuint(pos.y);
    z1 = TAUS_STEP_1;
    z2 = TAUS_STEP_2;
    
    uint x = HYBRID_TAUS_N2;
    return x;
}

프로토타입 선택하기
각 포인터에 적합한 프로토타입을 선택하는 것은 특히 컴퓨팅 셰이더를 사용할 때 복잡한 작업입니다. 난수를 생성하고 이를 프로토타입 목록에 매핑하는 것이 좋습니다. 여기에는 다양한 가중치를 가진 프로토타입을 고려하기 위한 버퍼를 생성하는 작업이 포함됩니다. 각 포인터에 대해 난수가 선택되고 이 버퍼의 인덱스에 매핑됩니다. 버퍼가 클수록 매핑이 더 정확해집니다.

프로토타입 매핑

대부분의 경우 공간을 고려한 난수를 고르기 위해 저는 풋프린트 크기와 거의 일치하도록 크기를 조정하는 블루 노이즈에 의존합니다.
블루 노이즈에 관해서는 이 기사를 적극 추천합니다: 

Free blue noise textures

Moments in Graphics A blog by Christoph Peters

momentsingraphics.de

왼쪽 : 유니폼랜덤 오른쪽: BlueNoise

특정 프로토타입의 덩어리를 피하는 것이 항상 그런 것은 아니며, 때로는 균일한 랜덤 또는 기타 노이즈 함수가 더 좋아 보일 수도 있습니다.

무작위 가중치 오프셋의 예
보로노이 노이즈를 웨이트 맵에 할당하고 랜덤 웨이트 오프셋을 조정합니다.

하나의 풋프린트에 많은 생태 유형이 있는 경우
하나의 풋프린트에 2개 이상의 생태형을 할당하는 경우 생태형 범위의 타겟팅을 상쇄하기만 하면 됩니다.

위의 그래프는 에코타입을 혼합할 수 있는 가능성 중 하나를 보여줍니다. 다른 하나는 새로운 밀도를 기준으로 이전 생태형을 덮어쓰는 것입니다. (저는 이 방법을 가장 많이 사용합니다.)

밀도
프로토타입의 밀도는 주로 프로토타입 선택 알고리즘에 의해 결정됩니다. 마스크 또는 노이즈 맵을 활용하여 여러 영역에 프로토타입을 배치하는 밀도를 제어할 수 있습니다. 이를 통해 지형 특징이나 플레이어의 동작에 따라 초목이나 기타 오브젝트를 동적으로 조정할 수 있습니다.

마스크 기반 밀도
지형 머티리얼에 따른 잔디 밀도

겹치는 인스턴스
절차적 배치의 맥락에서 겹치는 인스턴스를 관리하는 것은 미묘한 문제입니다. 이 문제는 주로 응답 거리와 프로토타입 밀도가 각각 다른 여러 개의 풋프린트를 함께 사용할 때 발생합니다. 예를 들어

풋프린트 A: 반응 거리가 0.5m로 주로 잔디를 흩뿌리는 용도로 설계되었습니다.
풋프린트 B: 반응 거리가 4.0m로, 나무의 산란에 사용됩니다.
이러한 시나리오에서는 풋프린트 A의 풀이 발자국 B의 나무와 같은 위치에 스폰될 수 있습니다. 이는 중복으로 보일 수 있지만, 자연스럽고 혼란스러운 환경의 느낌을 더하는 데 기여하는 경우가 많습니다. 자연 자체는 균일한 경우가 드물기 때문에 이러한 겹침이 의도치 않게 씬의 사실감을 더할 수 있습니다.

하지만 프로토타입 배치를 정밀하게 제어해야 하는 경우 정밀 밀도 마스크를 구현하여 이 문제를 해결할 수 있습니다. 이러한 마스크를 사용하면 특정 프로토타입을 배치할 수 있는 위치를 보다 정확하게 제어할 수 있으므로 원치 않는 겹침이 발생할 가능성을 효과적으로 줄일 수 있습니다. 이러한 마스크를 사용하면 나무가 있는 곳에 잔디가 자라지 않도록 하거나 그 반대의 경우도 방지하여 각 영역의 의도된 미적, 기능적 품질을 유지할 수 있습니다.

프로토타입 변형
프로토타입이 선택되면 변형 프로세스를 거칩니다. 여기에는 경사에 맞추기, 무작위 회전, 크기 조정 등이 포함됩니다. 이 변환 패스는 청크가 움직일 때와 업데이트가 필요한 포인터에 대해서만 실행됩니다.

다양한 발자국 배치 방법. 왼쪽부터: 유니폼스퀘어, 보로노이매치 4m, 보로노이매치 7m

 

다양한 로테이션 팜

애플리케이션/사용 사례 예시
동적 지형 생성: 서바이벌이나 탐험 장르와 같이 지형을 끊임없이 생성해야 하는 게임에서 사용합니다. 숲, 강, 산과 같은 지형 요소는 플레이어가 탐험하는 동안 동적으로 조정할 수 있습니다.
생물군계별 특징: 생물군계마다 고유한 생태 유형을 가질 수 있어 다양하고 사실적인 환경(예: 울창한 숲, 건조한 사막, 설산)을 구현할 수 있습니다.
실시간 환경 변경: 플레이어의 행동이나 이벤트에 따라 환경이 바뀌는 게임에 이상적입니다. 이 기능은 현재 개발 중인 게임에서 많이 필요한 기능입니다. 3인칭으로 플레이하면서 동시에 대규모 성을 건설할 수 있기 때문입니다. 성의 위치에서 식물을 효율적으로 제거할 수 있는 방법이 필요합니다.
시간에 따른 성장: 버려진 지역에서도 식물이 다시 자라나 세계 역학에 사실감을 더할 수 있습니다.