원문 : https://community.arm.com/developer/tools-software/graphics/b/blog/posts/mali-performance-7-accelerating-2d-rendering-using-opengl-es

주의 : 번역이 개판이므로 이상하면 원문을 참고하세요.

주의 : 허락받고 번역한 것이 아니므로 언제든 내려갈 수 있습니다.


성능 블로그를 쓴지 한참 지났습니다. 지난 밤동안 작성하고 싶은 블로그에 대한 점심 커피 토론 중 하나가 wasim abbas 가 만든 기술 데모가 되었습니다. 글을 다시 쓰게 만들어 준 그에게 감사드립니다. 이번에는 2D 렌더링과 OpenGL ES 가 도울 수 있는 부분에 대해서 살펴 보도록 하겠습니다.

매우 많은 모바일 칸텐트들은 여전히 2D 게이밍이거나 2D 유저 인터페이스 애플리케이션입니다. 애플리케이션들은 스크린상에 생성되는 스프라이트( sprite ) 레이어들이나 UI 요소( element )들을 렌더링합니다. 이런 거의 대부분의 애플리케이션들은 OpenGL ES 를 사용해 렌더링을 수행하는데요, 일부 애플리케이션들은 3D 기능들을 사용하며, 알파 투명도를 다루기 위해 블렌딩을 사용하는 전통적인 back-to-front 알고리즘의 단순함( simplicity )을 선호합니다.

이 접근법이 동작하기는 하지만, 하드웨어의 3D 기능들을 이용하지는 않습니다. 그래서 많은 경우에 GPU 는 필요 이상으로 더 빡세게( harder ) 작동합니다. 이것의 영향은 저성능부터 포함된 GPU 에 의존하는 배터리 수명까지 다양합니다. 그리고 이 영향들은 모바일 디바이스에서 고해상도를 향할수록 더 증폭되는 경향이 있습니다. 이 블로그는 스프라이트 렌더링 엔진들에 대한 간단한 변경사항들에 대해서 살펴 볼 것입니다. 그 엔진은 3D 렌더링 API 가 제공하는 도구들과 비교했을 때 렌더링을 훨씬 더 빠르게 만들어 주며, 에너지 효율이 더 높습니다.

Performance inefficency of 2D content

2D 게임들에서 OpenGL ES 프래그먼트 셰이더들은 보통 단순합니다( trivial ) -- 텍스쳐 좌표를 보간( interpolate )하고, 텍스쳐 샘플을 로드( load )하고, 프레임 버퍼에 블렌딩( blend )합니다 -- 그러므로 최적화할 부분이 많지 않습니다. 이러한 칸텐트 유형을 위한 성능 최적화는 거의 대부분 중복 작업을 완전히 없애는 방식을 찾는 것과 관련이 있습니다. 그러므로 일부 프래그먼트에서는 셰이더가 심지어는 아예 실행되지 않습니다.

소개 섹션에서의 그림은 배경 레이어에 위에서의 사각 스프라이트의 전형적인 블리트( blit, 역주 : 블록 데이터를 빠르게 메모리로 옮기거나 복사하는 기법 )를 보여 줍니다; 실드 스프라이트의 바깥 부분이 투명( transparent )이며, 경계 부분은 부분적으로 투명합니다. 그래서 배경으로 갈수록 에일리어싱( aliasing ) 아티팩트( artifact )없이 완전히 투명하게 사라집니다. 그리고 몸통 부분은 불투명( opaque )합니다. 이 스프라이트 블리트는 알파 블렌딩을 활성화한 상태로 프레임버퍼에서 back-to-front 렌더링 순서로 최상위에 렌더링됩니다.

여기에서 비효율성이 발생하는 두 가지 주요 원인이 존재합니다:

  • 먼저, 이 스프라이트를 둘러 싼 주변 영역들이 완전히 투명하므로, 그것은 출력 결과에 전혀 영향을 미치지 않고 있습니다. 하지만 처리하는데 시간은 걸립니다.
  • 둘째로, 스프라이트의 몸통 부분은 완전히 불투명하므로, 그 밑에 있는 배경 많은 픽셀들을 완전히 무시하고 있습니다. 그래픽스 드라이버는 배경이 무시되는 빠르게 알 수 없기 때문에, 이 배경 프래그먼트들이 GPU 에 의해 렌더링되어야만 합니다. 이는 최종 씬에 유용하게 영향을 주지 않는 뭔가를 렌더링하기 위해서 처리 사이클과 나노줄( nanojules )의 에너지를 낭비합니다.

이는 상대적으로 단일 오우버드로( overdraw ) 만이 존재하는 합성 예제이지만, 1080p 화면에서 렌더링된 프래그먼트가 절반 이상 중복되는 실제 애플리케이션을 볼 수 있습니다. 만약 애플리케이션들이 OpenGL ES 를 다른 방식으로 사용해 이 중복을 제거할 수 있다면, GPU 는 애플리케이션을 더 빠르게 렌더링할 수 있거나, clock rate 와 operating voltage 를 줄이기 위해서 생성된 성능 헤드룸( performance headroom )을 사용할 수 있을 것입니다. 즉 상당한 양의 에너지를 절약할 수 있습니다. 이런 결과들은 어느 하나라도 매력적입니다. 그래서 "애플리케이션 개발자가 이를 성취하기 위한 방법은 무엇인가?" 라는 질문을 할 수 있습니다.

Test scene

이 블로그를 위해서, 위의 실드 아이콘들을 cover-flow 스타일의 정렬로 구성된 간단한 테스트 씬을 렌더링할 것입니다. 하지만 그 기법은 불투명 영역을 가진 모든 스프라이트 셋에 대해 동작할 것입니다. 테스트 씬은 아래와 같이 렌더링합니다:

여기에서 각 실드 아이콘들은 실제로는 알파 투명도를 사용하여 보이지 않는 부분들을 가리는 사각 프라이트입니다.

Tools of the trade

전통적인 전용 2D 렌더링 하드웨어에는, 사용할 옵션들이 별로 없었습니다; 애플리케이션은 스프라이트 레이어를 back-to-front 로 렌더링해 블렌딩 함수가 올바로 작동하도록 보장해야만 했습니다. 우리의 경우에는 애플리케이션이 3D API 를 사용하여 2D 씬을 렌더링하며, 3D API 가 애플리케이션에 중복 작업 제거를 위해 제공하는 도구가 무엇이냐는 질문을 할 수 있습니다.

완전한 3D 씬을 렌더링하는 데 있어 중복 작업을 제거하기 위해 사용되는 기본 도구는 깊이( depth ) 테스트입니다. 삼각형 안의 모든 버텍스들은 그것의 위치에 "Z" 요소를 가지고 있으며, 이는 버텍스 셰이더로부터 노출됩니다. 이 Z 값은 버텍스와 카메라의 거리를 인코딩하며, 래스터화( rasterization ) 프로세스는 버텍스 값을 보간해서, 프래그먼트 셰이딩을 할 필요가 있는, 각 프래그먼트에 깊이를 할당합니다. 이 프래그먼트 깊이 값은 현재 깊이 버퍼에 저장된 현존하는 값들에 대해 검사될 수 있습니다. 만약 그 값이 프레임버퍼에 있는 현재 데이터보다 카메라와 가깝지 않다면, GPU 는 그 프래그먼트를 버리게( discard ) 되는데, 그려면 그것은 셰이더 코어에 제출되지 않습니다. 왜냐하면 그 프래그먼트가 불필요하다는 것을 안전하게 알고 있기 때문입니다.

Using depth testing in "2D" rendering

스프라이트 렌더링 엔진은 이미 각 스프라이트의 레이어링( layering )을 트래킹했습니다. 그래서 그것들은 올바른 순으로 쌓여( stack )있습니다. 그래서 우리는 이 레이어 번호를 GPU 에 보내진 각 스프라이트의 버텍스에 할당된 Z 좌표 값으로 매핑할 수 있습니다. 그리고 실제로 그것이 3D 깊이를 가진 것처럼 씬을 렌더링할 수 있습니다. 만약 깊이 어태치먼트( attachment )를 가진 프레임버퍼를 사용하고, 깊이-쓰기( depth-write )를 활성화하고, 스프라이트들과 배경 이미지를 front-to-back 순서로 렌더링한다면( 예를 들어 back-to-front 인 일반적인 블리팅 패스의 반대 순서로 ), 깊이 테스트가 스프라이트와 백그라운드의 다른 스프라이트에 의해 가려지는 부분을 제거할 것입니다다.

이를 우리의 간단한 테스트 씬을 위해 실행하면, 다음의 결과르 얻을 수 있습니다:

오! 뭔가 잘못되었습니다.

여기에서 문제는 사각 스프라이트 지오메트리가 정확하게 불투명 픽셀의 모양과 일치하지 않는다는 것입니다. 카메라와 더 가까운 스프라이트의 투명 부분이 알파테스트로 인해 어떠한 컬러 값도 생성하지 않고 있음에도 여전히 깊이 값을 설정하고 있습니다. 아래쪽 레이더의 스프라이트가 렌더링될 때, 깊이 테스트를 하면 이전의 스프라이트의 투명 부분의 밑에서 가시적이어야만 하는 부분들이 부적절하게 제거( kill )된다는 것을 의미합니다. 그래서 OpenGL ES 클리어 컬러만이 보이고 있습니다.

Sprite geometry

이 문제를 해결하기 위해서, 스프라이트를 위해 유용한 지오메트리를 설정하는 방법에 대한 연구를 좀 할 필요가 있었습니다. 스프라이트 내에서 완전히 불투명한 픽셀을 위해 front-to-back 으로 렌더링할 때 깊이 값을 안전하게 설정할 수 있습니다. 그래서 스프라이트 아틀라스 생성은 각 스프라이트를 위해 두 가지 셋( set )을 제공할 필요가 있습니다. 한 셋은 아래의 가운데 이미지에서의 녹색 영역으로 지정되어 있으며, 이는 불투명 지오메트리만을 다룹니다. 그리고 두 번째 셋은 아래의 오른쪽 이미지에서 녹색 영역으로 지정되어 있으며, 완전히 투명( 완전히 버려질 수 있는 경우 )하지 않은 모든 것들을 포함하고 있습니다.

버텍스들은 상대적으로 비쌉니다. 그래서 이런 지오메트리 셋을 생성할 때는 가능한 한 적은 개수의 부가적인 지오메트리들을 사용하십시오. 불투명 영역은 완전히 불투명한 픽셀들만을 포함해야 합니다. 하지만 투명 영역은 안전하게 불투명 픽셀들과 완전하게 투명한 픽셀들을 부수 효과( side-effect )없이 포함할 수 있습니다. 그러므로 "best fit" 를 획득하려고 시도할 필요없이 "good fit" 를 위해 대충 근사계산하십시오. 일부 스프라이트의 경우에는, 불투명 영역을 생성할 가치가 없다는 것을 기억하세요( 불투명 텍셀을 가지고 있지 않거나 매우 작은 영역만이 불투명할 때 ). 그래서 일부 스프라이트들은 투명 렌더링으로 렌더링될 단일 영역만으로 구성될 수도 있습니다. 경험적으로 볼 때, 불투명 영역이 256 픽셀보다 적을 때, 그것들은 부가적인 지오메트리 복잡성을 사용해 경계처리될 가치가 없습니다. 하지만 항상 시도하고 확인할 가치는 있습니다.

이런 지오메트리를 생성하는 것은 상대적으로 성가십니다. 하지만 스프라이트 텍스쳐 아틀라스들은 보통 정적이며 이것은 애플리케이션 칸텐트 저작 과정에의 일부로 오프라인으로 수행될 수 있습니다. 그리고 런타임에 플랫폼 상에서 라이브로 수행될 필요가 없습니다.

Draw algorithm

각 스프라이트에 대해 두 개의 지오메트리 셋을 사용하여, 이제 테스트 씬의 최적화된 버전을 렌더링할 수 있습니다. 먼저 불투명 스프라이트 영역들과 배경을 font-to-back 순서로 렌더링합니다. 이 때 깊이 테스팅과 깊이-쓰기를 활성화합니다. 그 결과는 다음과 같습니다: 

다른 스프라이트 밑에서 가려진 스프라이트나 백그라운드의 영역은 절약될 수 있는 렌더링 작업입니다. 왜냐하면 그것은 셰이딩이 발생하기 전에 빠른 깊이 테스트를 통해 제거되었기 때문입니다.

불투명 지오메트리를 렌덜이했다면, 이제 각 스프라이트의 투명영역을 back-to-front 순서로 렌더링할 수 있습니다. 깊이 테스트를 켜 둔 채로 남겨 놨기 때문에, 아래쪽 레이어들의 스프라이트들은 논리적으로 위쪽에 있는 레이어들의 불투명 영역을 덮어쓰지 않습니다. 하지만 조금이라도 전력을 절약하려면 깊이 버퍼를 비활성화하십시오.

만약 불투명 스테이지의 컬러 출력을 지워버리고 깊이 값을 유지하고 투명 패스를 그리면, 이 패스에 의해서 추가된 부가적인 렌더링 결과를 가시화할 수 있습니다. 이는 아래 그림처럼 보입니다:

링들 외부의 불완전한 영역들은 작업이 절약된 영역을 가리킵니다. 왜냐하면 없어진 부분들은 첫 번째 드로잉 패스에서 렌덜이된 카메라에 더 가까운 불투명 스프라이트 영역의 깊이 값을 사용하는 깊이 테스트에 의해 제거되었기 때문입니다.

두 패스를 모두 같은 이미지에 함께 배치하고 렌더링하면, 원래의 back-to-front 렌더링과 동일한 가시적 결과를 다시 얻을 수 있습니다:

하지만 35 % 정도 더 적은 프래그먼트 스레드들이 시작되었고, 이는 이 씬을 렌더링하기 위해서 필요한 MHz 의 35 % 를 줄인 것으로 해석됩니다. 성공적입니다!

필요로 하는 마지막 연산 논리( operational logic )는 우리가 씬에 추가한 깊이 버퍼가 메모리에 다시 쓰여지지 않음을 보장하는 것입니다. 만약 애플리케이션이 EGL 윈도우 서피스에 직접적으로 렌더링하고 있다면, 여기에서 할 일은 없습니다. 왜냐하면 윈도우 서피스를 위한 깊이가 묵시적으로 버려지기 때문입니다. 하지만 여러분의 엔진이 오프스크린 FBO 에 렌더링하고 있다면, 오프스크린 타깃으로부터 FBO 바인딩을 변경하기 전에 ( OpenGL ES 3.0 이상에서는 ) glInvalidFramebuffer() 이나 ( OpenGL ES 2.0 에서는 ) glDiscardFramebufferEXT() 에 대한 호출을 추가하는 것을 보장하십시오. 세부사항을 원한다면 이 블로그를 확인하세요.

Summary

이 블로그에서 우리는 깊이 테스팅과 깊이-지향( depth-aware ) 스프라이트 기법의 사용이 3D 그래픽스 하드웨어를 사용하는 렌더링을 가속하는데 사용되는 방법에 대해서 살펴 보았습니다.

스프라이트의 불투명 영역과 투명 영역 사이의 분리를 제공하기 위해 부가적인 지오메트리를 추가하는 것은 복잡도를 증가시킵니다. 그래서 스프라이트들을 위한 버텍스의 개수를 최소화하려고 해야 하며, 그렇지 않으면 부가적인 버텍스와 작은 삼각형 크기를 처리하는데 드는 비용이 이점을 없애버릴 것입니다. 부가적인 지오메트리가 너무 복잡해질 것이 요구되거나 화면을 덮고 있는 영역이 너무 작다면, 그냥 불투명 영역을 무시하고 전체 스프라이트를 투명인 것처럼 렌더링하십시오.

이 기법이 3D 게임에서 2D UI 요소들을 렌더링하기 위해서도 사용될 수 있다는 것을 언급할 가치가 있습니다. 3D 씬을 렌더링하기 전에 near clip plane 과 매우 가까운 깊이로 UI 의 불투명 부분을 깊이와 함께 렌더링하십시오. 그리고 나서 3D 씬을 일반적인 경우처럼 렌더링하십시오( 불투명 UI 요소들의 뒤에 있는 부분들이 건너뛰어지게 될 것입니다 ). 마지막으로 UI 의 투명 부분들이 3D 출력의 최상위에서 렌더링되고 블렌딩될 수 있습니다. 3D 지오메트리들이 UI 요소에 간섭하지 않는다는 것을 보장하기 위해서 glDepthRange() 를 사용해 3D 패스에 의해 노출되는 깊이 값들의 범위를 매우 약간 제한할 수 있습니다. 이는 UI 요소들이 3D 렌더링보다 항상 near clip plane 에 더 가깝도록 보장해 줍니다.

Tune in next time,

Pete.

+ Recent posts