01. GPU의 의미
그래픽 처리를 위한 시스템은 GPU를 중심으로 구성된 칩셋에서 이뤄짐.
CPU가 렌더링 명령을 내리면 GPU가 수행.
GPU 메모리, VRAM
Video Random Access Memory.
CPU가 메모리에서 데이터를 읽어오듯 GPU도 그래픽 카드에 GPU 메모리가 있다! 그것이 VRAM.
텍스처, 메시 데이터 등 렌더링에 필요한 데이터들, 렌더링 결과를 저장하는 버퍼들이 포함됨.
렌더링할 때 여기에 저장된 데이터를 참고해서 그래픽 처리.
02. 게임 루프
유저가 게임을 실행하면
로딩(초기화, 리소스 생성) -> 게임 중(매 프레임 렌더링) -> 게임 종료(리소스 해제)
위와 같은 일련의 과정이 게임 루프.
만약 10프레임이라면, 1초동안 10개의 장면, 한 장면당 0.1초를 쓰게 됨.
매 프레임마다
물리연산, 입력신호, AI, 게임 로직처리, 애니메이션 모션 처리, 네트워크 처리, 오디오 처리...
등등 매우 다양한 연산을 거쳐야 한다. 그리고 최종 형태를 결정하고 그것을 렌더링하는 것까지. 이것을 반복.
크게 과정을 나눠보면
Initialization _ Awake, OnEnable, Start 호출
↓
Update _ Physics, Input Events, Game logic 처리
↓
Render _ Scene, GUI rendering
↓
Decommissioning
* 렌더링은 업데이트가 모두 끝난 후!
아래는 유니티 매뉴얼에 나와있는 flowchart.
03. 렌더링 루프
렌더링 과정을 살펴보면
업데이트가 모두 끝나고 오브젝트의 위치나 모습등이 결정되면 이제 그것들을 렌더링 해줘야하는데 이것도 순서가 있다. 모든 오브젝트를 렌더링하면 프레임이 끝나고 비로소 화면에 출력.
04. 렌더링 파이프라인
== 오브젝트를 2D로 그리는 과정.
오브젝트를 한번에 뿅! 그리는게 아니라 일련의 과정을 거치면서 그려진다는 얘기.
오브젝트를 렌더링하기 위해서는
- 형태를 나타내는 Mesh 정보
- Albedo, Normal, Specular 등의 텍스처 정보
- 조명을 어떻게 처리할지 결정하는 Shader 정보
- Transform (위치, 회전, 스케일) 정보
등의 많은 정보가 필요하다. 이 정보들은 하나의 오브젝트가 렌더링되는 과정에서 쓰인다.
여기서는 파이프라이는 크게 세 파트로 나누어 설명한다.
Application, Geometry, Rasterizer
Application
애플리케이션 상에서 처리되는 단계. 오브젝트 렌더링 이전에 업데이트에서 데이터를 처리하는 단계.
CPU에서 연산하는 거라(100% CPU만 쓰는 것은 아님) 엄밀히 말하면 렌더링 파이프라인에 속하지 않지만 렌더링 이전에 필요한 연산을 처리하는 것으로 반드시 거쳐야하는 단계이기 때문에 크게 보면 파이프라인에 껴줄 수 있음.
이 단계에서 수행되는 연산들은 최종적으로 렌더링 파이프라인 성능에 영향을 주기 때문!
현재 프레임에서 렌더링 가능한 오브젝트들을 컬링을 통해 선별 --> 오브젝트가 줄어든만큼 GPU 부담 적어짐
Geometry
버텍스, 폴리곤 처리. 오브젝트의 버텍스를 화면상에 배치.
Vertex Transform
렌더링을 수행할 시점이 되면 GPU는 GPU 메모리에 저장되어있는 버텍스 정보들을 가져옴.
이 버텍스 정보들은 메시 모델에 대한 위치 데이터만 가지고 있음 (얘가 어디 배치될지 모르니까)
이를 로컬 스페이스(local space)에 존재한다고 표현한다.
모델 그 혼자만의 공간을 로컬 스페이스라고 생각하면 된다.
얘를 3D 공간 상에 배치하려면 오브젝트의 Transform 정보에 따라 위치를 이동시켜줘야한다. Transform 정보를 반영해주면 Local에서 World space로 데뷔한다. 이렇게 World space로 변환시켜주는 것을 World Transform이라고 한다.
이제 얘는 월드 공간에 배치되었다!
근데 또 카메라 스페이스로 변환시켜주어야한다. 우리는 얘를 카메라로 보고 있기 때문.
카메라 역시 고유의 위치, 방향을 가지고 있는데 (스케일은 없다) 카메라의 공간은 Camera Space 혹은 View Space라고 한다. 그러면 역시 View Space로 변환시켜주는 것은 View Transform이라고 부르겠지.
자 이제 얘는 카메라 앞에도 설 수 있게 되었다.
그치만 우리는 3D가 아닌 2D에 렌더링해야한다. 그래서 최종적으로 이 3D 공간을 2D 상의 위치에 매칭시켜야한다.
매칭시켜주는 과정을 Projection, 투영이라고 한다.
Perspective Projection, 원근 투영
카메라에서 빔 프로젝트를 쏜 것처럼 뷰 프러스텀이 생긴다. 이 경우 원근법이 적용된다. 같은 크기의 물체라도 카메라와의 거리에 따라 크기가 다르게 보여진다. 원리는 그림을 통해 설명해야할 것 같으므로,,,게임수학을 복습할 때 포스팅해야겠다.
Orthographic Projection, 직교 투영
뷰 프러스텀이 직육면체 형태로, 원근법이 적용되지 않은 투영이다. 카메라와의 거리가 무시되고 원래의 크기대로 보여진다.
Vertex Shader
트랜스폼 변환들은 버텍스 쉐이더에서 이루어진다.
Shader라고 해서 조명 처리를 할 것 같지만 .. Vertex Shader에서는 트랜스폼 처리를 한다.
월드-뷰-프로젝션 트랜스폼은 버텍스 쉐이더에서 행렬 연산을 통해 수행된다. 이 행렬연산도 게임수학..에서..
메쉬의 버텍스에다가 이 행렬을 곱해줘서 버텍스를 알맞은 위치에 배치시켜주는 일을 Vertex Shader가 하는 것.
위치 뿐 아니라 Normal, Color도 버텍스 쉐이더에서 결정된다. 일반 조명은 픽셀별로 처리되는데 만약 버텍스 별로 처리되는 조명이라면 버텍스 쉐이더에서 처리를 하고 이때 버텍스의 컬러에 조명 결과를 반영한다.
Geometry 생성
버텍스 쉐이더를 거치고 나면 이제 버텍스들은 서로 연결되어 형태를 이룰 것인데 그래픽스에서는 이 Mesh의 형태를 Geometry라고 부른다. Geometry의 생성은 버텍스 쉐이더를 거치면 다음 단계에서 자동으로 이루어지는 것이다.
그래픽스 API 버전에 따라 최신버전에서는 지오메트리 생성단계에서 지오메트리 쉐이더, 헐 쉐이더, 도메인 쉐이더 등을 거쳐 테셀레이션을 수행할 수 있다. 테셀레이션은 원래 있던 버텍스에서 추가로 버텍스를 생성해서 메쉬를 더 잘게 쪼개 표현을 더욱 부드럽고 세밀하게 할 수 있는 기법이다. 이것도 게임수학에서 더 자세히 ㅎㅎ
Rasterizer
메쉬가 화면에 매칭되는 픽셀을 결정하고 최종적으로 색을 입히는 과정. 이 때 메쉬의 폴리곤에 속한 영역을 픽셀로 매칭시키는 과정을 Rasterization이라고 함.
버텍스 쉐이더를 거쳐서 지오메트리가 생성되었으므로 이제 화면에 어떤 픽셀에 그려져야하는지 결정된 것이다.
Z Buffer (=Depth Buffer)
픽셀은 크게 두가지의 버퍼에 정보를 저장한다. 픽셀의 최종 색상 정보는 Color Buffer, 카메라로부터의 거리인 깊이값은 Z Buffer에 저장한다. 픽셀을 렌더링 할 때마다 이 Z Buffer를 통해 깊이 판정을 수행한다. 이것을 Z 테스트라고 한다.
이 부분은 조금 자세하게 써져있어서 게임수학으로 미루지 않고 간단히 요약해본다.
Z Buffer에 저장되어있는 픽셀의 깊이 값과 지금 출력하고자하는 픽셀의 깊이 값을 비교해서 지금 출력하고자 하는 픽셀이 더 앞에 있다면 (깊이값이 작은 것이 앞에 있는 것) 출력하고, 뒤에 있다면 출력하지 않는 것이다. (일반적으로 뒤에 있으면 가려져서 보이지 않으니까 괜히 렌더링하지 않는 것이다. 계획이 있다면 반대로 작동시킬 수 있다)
Z 테스트를 수행하면 가려지는 것을 알 수 있으므로 요즘의 그래픽 칩셋들은 Fragment Shader 이전에 Z 테스트를 수행해서 미리 가려지는 픽셀들을 걸러내어 비용을 절약한다.
Fragment Shader(=Pixel Shader)
픽셀들의 최종 색을 계산.
텍스처로부터 색상을 읽어오고 그림자도 적용. 픽셀별 조명 처리를 한다면 조명 연산을 반영한 색상을 입힘.
Blending
투명도가 있는 오브젝트라면 픽셀 렌더링 시 알파 블렌딩을 거친다.
쉐이더에서 결정된 Alpha 값을 이용하여 해당 위치의 기존 컬러 버퍼 값과 지금 출력하고자 하는 픽셀의 컬러 버퍼 값을 섞어서 최종 색상을 결정한다.
05. 정리
오브젝트들은 앞의 일련의 과정들을 거쳐 화면에 렌더링 된다.
화면에 바로 렌더링 되는 것은 아니고, 버퍼에 차곡차곡 쌓여서 그려지는 것이다.
Double Buffering
버퍼를 두개 사용하는 것이다.
버퍼 1, 2가 있으면 1이 화면에 나타나서 시선을 끄는 동안 버퍼 2에 프레임을 렌더링한다. 버퍼 2의 렌더링이 끝나면 얘를 보여주고 버퍼 1은 렌더링을 시작한다. 이렇게 번갈아 보여주면서 다음 프레임을 렌더링할 시간을 버는 것이다. 이때 지금 출력되고 있는 버퍼가 Front Buffer, 뒤에서 렌더링 중인 버퍼가 Back Buffer이다.
렌더링 루프를 쭉 정리해보자면...
- 프레임이 시작되고 렌더링 이전에 업데이트가 이루어짐. 물리, 입력, 로직, 애니메이션 등 연산을 수행하고 컬링 연산으로 불필요한 렌더링 부하를 방지한다.
- 오브젝트를 렌더링한다. 오브젝트마다 드로우 콜이 발생하며 GPU 파이프라인에 따라 버퍼에 순차적으로 렌더링된다.
- 포스트 프로세싱을 처리한다. 얘도 버퍼에 렌더링을 하는 것으로 한번 이상의 드로우 콜이 발생한다. 포스트 프로세싱에서는 특정 메쉬를 그리는 것이 아니라 화면을 덮는 사각형을 그리기 때문에 대부분의 처리가 버텍스 쉐이더가 아닌 프래그먼트 쉐이더에서 이루어진다.
- 모든 요소가 렌더링이 되면 화면에 버퍼들이 교체되면서 디스플레이된다.
- 디스플레이가 되고 나면 한 프레임이 끝나고 ,, 이제 다음 프레임으로 넘어가서 업데이트부터 다시 반복한다.
'게임엔진 > Unity' 카테고리의 다른 글
[Unity] 그래픽스 최적화 - 드로우콜과 배칭 (0) | 2023.08.12 |
---|---|
[Unity] 그래픽스 최적화 - 병목 (0) | 2023.08.12 |
[Unity] 내 주변을 원 모양으로 퍼지는 탄환 생성 (0) | 2023.08.04 |
[Unity] 내 주변을 원 운동하는 오브젝트 만들기 (0) | 2023.08.04 |
[Unity] Sprite Sheet (스프라이트 시트)를 사용한 2D 애니메이션 (0) | 2023.08.04 |