Post Lists

2018년 12월 23일 일요일

Terrain Rendering Article 1

https://blogs.igalia.com/itoral/2016/10/13/opengl-terrain-renderer-rendering-the-terrain-mesh/

Terrain Rendering을 위해 이걸 번역하도록 하겠다.

=========================================================
OpenGL terrain renderer: rendering the terrain mesh

이 포스트에서, 나는 OpenGL terrain rendering demo에서 terrain mesh를 어떻게 설정하고 렌더링하는지를 이야기할 것이다. 이것과 관련된 대부분의 코드는 ter-terrain.cpp 파일에 있다.

Setting up a grid of vertices
만약 너가 3D 모델링 프로그램을 어떻게 적절히 사용하는지를 모른다면, terrain을 위한 멋있는 mesh를 만드는 합리적인 방법은 정점들의 한 grid를 사용하고, height map image에 따라서 그것들을 높이는 것으로 구성된다. grid를 만들기 위해서, 우리는 우리가 얼마나 많은 rows와 columns를 원하는지를 결정할 필요가 있따. 끝으로, 이것은 폴리곤의 개수와 terrain의 resolution을 결정한다.

우리는 이러한 정점들을 월드 좌표로 매핑할 필요가 있다. 우리는 tile size를 정의하여 그렇게 하는데, 그 타일 사이즈는 world units에서 연속적인 정점 사이의 거리이다. 더 큰 타일 사이즈는 테레인의 크기를 증가시키지만, 더 큰 폴리곤을 만들어서 해상도를 낮추게 된다.

아래의 이미지는 35개의 타일들을 정의하는 8x6 grid를 보여준다. 각 타ㅣㅇㄹ은 2개의 삼각형들을 사용하여렌더링된다.

그 다음 단계는 이러한 정점들을 elevate 시키는 것이다. 그래서 우리는 지루한 평평한 평면을 갖지 않게 된다. 우리는 grid에서 각 정점에 대해 height map image를 샘플링하여 이것을 한다. height map은 gray scale image인데, 거기에서 픽셀들의 값은 다른 위치에 있는 고도를 나타낸다. 그 color가 white에 가까울 수록, 그것은 좀 더 elevated 되어있다.

grid에 더 많은 정점들을 넣을 수록 그 height map으로부터 sampling points의 개수를 증가시키게 되고,  sampling distances가 줄어든다. 그리고 이것은 더 부드럽고 좀 더 정확한 height map의 표기를 이끌게 된다. 최종 테레인에서.

물론, 우리는 height map samples를 여전히 world units에서 고도로 매핑시킬 필요가 있다. 데모에서, 나는 color values를 [-1, +1]로 표준화하여 이것을 하고, world space에서 고도 값을 계산하기 위해 scale factor를 적용한다. scaling factor로 가지고서, 우리는 우리의 terrain이 좀 더 급작스럽지 않게 보이도록 할 수 있다.

참고를 위해서, height map sampling은 ter_terrain_set_heights_from_texture() 에 구현되어 있다.

Creating the mesh
이 시점에서, 우리는 우리의 grid에 있는 모든 정점들의 월드 좌표에서의 완전한 위치 (x,y,z)를 안다. 그 다음 단계는 terrain을 렌더링하고 각 삼각형에 대한 normal vectors를 렌더링하는데 사용할 실제 triangle mesh를 구성할 것이다. 이 프로세스는 아래에서 설명되고, ter_terrain_build_mesh() 함수에 구현되어 있다.

Computing normals
우리의 terrain에서 좋은 lighting을 얻기 위해서, 우리는 mesh에서 각 정점에 대한 normals를 연산할 필요가 있다. 이것을 하는 간단한 방법은 각 face (triangle)에 대한 normal을 연산하고, 삼각형에서 각 정점에 대한 그 normal을 사용하는 것이다. 이것은 작동하지만, 3개의 문제를 가진다.


  1. 각 삼각형의 모든 정점은 정확히 같은 normal을 갖게 되고, 이것은 오히려 flat result로 이끈다.
  2. 다른 방향들을 가진 인접한 삼각형들은 normal value에서 갑작스러운 변화를 보여주게 되고, 이것은 mesh에서 개별 삼각형들을 강조시키는 surfaces 넘어서 다른 lighting을 보여주게 된다.
  3. mesh에서 각 정점은 그것이 참여하는 각 삼각형에 대해 다른 normal value를 가질 수 있기 때문에, 우리는 우리가 렌더링할 때 최적지 아닌 정점들을 복사할 필요가 있을 것이다.
대안적으로, 우리는 그것의 인접 정점들의 높이를 고려하는 각 정점들에 대한 normals를 연산할 수 있다. 이것은 위에서 언급된 모든 문제들을 해결하고, 삼각형들을 가로지르는 normal vectors의 interpolation덕분에 더 좋은 결과들을 이끈다. 이것은 smooth lighting reflection transitions을 이끌게 된다.

이것에 대한 구현은 calculate_normal()에 있다. 그리고 이것은 grid에서 정점들의 column과 row indices를 받고, grid에서 4개의 가까운 정점들의 높이를 샘플링하여 Y 좌표를 계산한다.

Preparing the draw call
우리가 모든 정점들의 위치와 그것들의 normal vectors를 알고 있기 때문에, 우리는 테레인을 렌더링하는데 필요한 모든 정보를 가지고 있다. 우리는 여전히 모든 폴리곤들을 얼마나 정확히 렌더링하고 싶은지를 결정할 필요가 있다.

single draw call을 사용하여 테레인을 렌더링하는 가장 간단한 방법은 메쉬에서 (position과 normal information을 포함한) 각 삼각형에 대한 데이터를 가진 vertex buffer를 설정하는 것이고, draw call의 primitive에 대해 GL_TRIANGLES를 사용하는 것이다. 그러나, 이것은 성능의 관점에서 최상의 옵션은 아니다.

테레인은 일반적으로 많은 수의 정점들을 포함하고 있고, 그것들 대부분이 다양한 삼각형에 참여하고 있기 때문에, 우리는 GPU에 많은 양의 정점 데이터를 업로드하고 그 draw call에서 많은 정점들을 처리하게 된다. 그 결과는 큰 메모리 요구사항과 suboptimal performance이다.

참고를 위해서, 내가 나의 원래 글의 데모에서 사용했던 테레인은 251x251 grid를 사용했었다. 이 그리드는 250x250 tiles를 나타내고, 각 타일은 두 개의 삼각형으로 렌더링 되어진다 (6 vertices/tile), 그래서 우리는 250x250x6 =375,000 정점들을 가지게 된다. 이러한 정점들 각각에 대해, 우리는 position과 normal를 가진 vertex data의 24 bytes를 업로드 할 필요가 있다. 그래서 우리는 거의 9MB 크기를 가진 GPU buffer를 가지게 될 것이다.

이것을 줄이는 한 가지 명백한 방법은 triangle strips를 사용항 테레인을 렌더링하는 것이다. 이것과 관련한 문제는, 이론적으로 우리는 한 strip으로 terrain을 렌더링할 수 없다는 것이다. 우리는 tile colume당 one strip (그래서, one draw call) 또는 tile row당 한 strip을 필요로 할 것이다. 운 좋게도, 우리는 각 column에 대해 분리된 strips를 한 개의 draw call로 연결시키기 위해 degenerate triangles를 사용할 수 있다. 이것으로, 우리는 정점들의 개수를 126,000 개로 줄일 수 있고, 버퍼 사이즈는 3MB 아래로 떨어진다. 이것은 데모에 단독으로 15% ~ 20%의 성능 향상을 만들어냈다.

우리는 그런데 더 잘할 수 있다. terrain mesh에 있는 많은 정점들은 그 draw call에서 large triangle strip을 넘어서 여러 삼각형에 참여하는데, 그래서 우리는 strip을 렌더링하는데 index buffer를 사용하여 메모리 요구사항을 줄일 수 있다. 만약 우리가 이것을 한다면, 우리는 63,000 정점들로 줄일 수 있고, ~1.5MB로 줄일 수 있따. 이것은 원래 구현에 대해 4% ~ 5% 성능 보너스를 추가한다.

Clipping
지금까지, 우리는 각 프레임에서 terrain의 전체 mesh를 렌더링 해왔고, 우리는 vertex data를 GPU에 한 번 업로드하여 (예를들어 첫 프레임에) 이것을 한다. 그러나, 카메라가 위치한 곳과 어디를 바라보고 있는지를 기반으로, terrain의 한 fraction만이 보일지도 모른다.

비록 GPU가 viewport 밖에 있는 geometry와 fragments를 버릴지라도, 그것은 여전히 그것이 보이지 않는 삼각형을 처리하기 전에 vertex shader stage에서 각 정점들을 처리해야만 한다. terrain의 삼각형의 개수가 크기 때문에, 이것은 suboptimal이고, 이것을 다루기 ㅜ이해서, 우리는 우리가 렌더링하기 전에 CPU-side clipping을 할 필요가 있다.

CPU-side clipping을 하는 것은 몇 가지 부가적인 복잡성이 오게 된다: 그것은 우리가 terrain의 보이는 영역을 계산하고, 새로운 정점 데이터를 각 frame prevention GPU stalls에 있는 GPU 업로드하기를 요구한다.

데모에서, 우리는 우리가 렌더링할 필요가 있는 보이는 구역을 포함하는 terrain의 quad sub-region을 연사하여 clipping을 구현한다. 우리가 렌더링하고 싶은 sub-region만 알기많 나다면, 우리는 그 구역에 참여하는 정점들의 새로운 인덱스들을 연산한다. 그래서 우리는 single triangle strip을 사욯아여 그것을 렌더링할 수 있다. 마침내, 우리는 그 새로운 index data를 follow-up draw call에 사용하기 위해 index buffer를 업로드 한다.

Avoiding GPU stalls
위의 모든 것이 정확하다면, 그것은 일반적으로 설명된대로 일반적으로 더 악화된 성능을 이끈다. 이것에 대한 이유는, 각 프레임에서 정점 데이터의 업로드를 하는 것은 빈번한 GPU stalls를 이끌게 된다. 이것은 두 가지 시나리오에서 발생한다:

  1. 같은 프레임에서, 우리는 shadow map과 scene을 위한 terrain rendering을 위한 다른 정점 데이터를 업로드 할 필요가 있기 때문이다 (shadow map은 light의 관점에서 terrain을 렌더링하고, 그래서 그 terrain의 보이는 영역이 다르다). 이것은 stalls를 만드는데, shadow map을 위한 terrain rendering은 scene을 위한 테레인을 렌더링하기 위해 index buffer에 새로운 데이터를 업로드 하기전에 완료되지 않을지도 모르기 때문이다.
  2. 다른 프레임들 사이에서, 우리가 다음 프레임을 준비하기 시작하고, 그것을 위해 새로운 terrain index를 업로드하려고 시도하기 전에 GPU는 완전히 이전 프레임을 렌더링하는데 끝나지 않았을지도 모르기 때문이다. (따라서, 여전히 이용가능한 index buffer data가 필요하다.)
Intel Mesa driver의 경우에, environment variable INTEL_DEBUG=perf를 사용하여 이러한 GPU stalls은 쉽게 확인될 수 있다. 이것을 사용할 때, 그 드라이버는 이러한 상황을 탐지하고, stalls에 대한 경고 정보를 만들어낼 것이다. 그 정보는 영향을 받는 버퍼이고, stall을 만드는 buffers의 영역들이다 다음과 같이.

Stalling on glBufferSubData(0, 503992) (492kb) to a busy (0-1007984) buffer object. Use glMapBufferRange() to avoid this.

내가 구현한 이 문제에 대한 해결책은 두 가지 형태이다 (index buffer에 대한 read/write 접근들 사이에 가능한 많은 일들을 넣는것 외에) :

1. Circular buffers

이 경우에, 우리는 새로운 index data의 각 subsequent upload가 할당된 버퍼의 separate sub-region에 발생하도록 하기 위해 우리가 필요한 것 보다 더 큰 버퍼를 할당한다. 나는 각 circular buffer가 각 프레임에서 발생하는 index buffer의 모든 업데이트에 대해 요구되는 index data를 충분히 유지할 만큼 크도록 데모를 설정했다.

2. Multi-buffering
우리는 한 circular buffer 이상을 할당한다. 우리가 새로운 index buffer data를 업로드시킬 현재 버퍼의 끝에 충분한 free space가 없을 때, 우리는 그것을 대신에 다른 circular buffer에 업로드 시킨다. 우리가 버퍼가 부족할 때, 우리는 첫 번째 것을 다시 순환시킨다 (이 시점에서, 희망스럽게 또 다시 자유롭게 재 사용될 것이다).

그래서 왜 단일의 매우 큰 circular buffer를 사용하지 않는가? 대개 GPU가 정확히(또는 효율적으로) 처리할 수 있는 버퍼의 크기에 제한이 있기 때문이다. 또한, 왜 우리는 circular buffers 대신에 많은 작은 independent buffers를 갖지 않는가? 그것은 잘 작동할 것이지만, fewer, larger buffers를 사욯아는 것은 우리가 bind/unbind 시킬 필요가 있는 오브젝트들의 수를 줄이고, 메모리 파편화를 방지하는데 더 좋다. 그래서 그것은 부가요인이다.

Final touches
우리는 거의 다 했다. 이 시점에서, 우리는 오직 terrain surface에 텍스쳐를 더하고, 좀 더 현실적인 look을 만들기 위해 먼 거리의 픽셀에 대해 어떤 약한 fog effect를 더하고, skybox를 더할 필요가 있다 (fog의 색을 선택하는 것은 중요한데, 그래야 그것이 sky의 컬러와 어울린다!) 그리고 좋은 결과를 위해 lighting parameters를 tweak 해야 한다.














댓글 없음:

댓글 쓰기