10.1 깊이 버퍼를 이용한 삼각형 래스터화의 기초

시각적인 사실성을 희생해서라도 실시간 성능을 얻고자 하는 방식부터 포토리얼리즘을 위해서 실시간 동작을 포기하는 방식까지 다양한 형태의 렌더링 기법이 존재한다.

 

10.1.1.1 고성능 렌더링 프로그램에서 쓰이는 형식

영화의 컴퓨터 그래픽에서는 표면을 주로 사각형 패치로 나타내는데, 패치는 적은 수의 컨트롤 포인트에 의해 정의되는 2차원 스플라인으로 구성된다. 사용되는 스플라인의 종류는 베지어 표면, NURBS, 베지어 삼각형, N-패치 등이 사용된다.

 

픽사의 렌더맨(RenderMan)같은 고급 영상용 렌더링 엔진은 기하 형상을 정의하는데 분할 표면을 사용한다. 모든 표면은 컨트롤 다각형으로 이루어진 메시로 표현하는데, 캣멀-클락(catmull-Clark) 알고리즘을 이용하면 다각형을 계속 더 작은 다각형으로 재분할 수 있는 특징이 있다. (거리에 따른 LOD 수준을 조정할 수 있다.)

 

10.1.1.2 삼각형 메시

삼각형을 실시간 렌더링에 사용하는 이유

ㄴ 표면을 만들 수 있는 가장 작은 단위의 다각형이기 때문 (단순함)

ㄴ 삼각형은 언제나 평평하다. (정점들이 한 평면에 존재함이 보장됨)

ㄴ 시중에 존재하는 거의 모든 그래픽 가속 하드웨어는 삼각형 래스터 변환으로 디자인되어 있다.

 

테셀레이션 

표면을 여러 개의 분할된 다각형으로 쪼개는 과정이다. 게임에 쓰이는 삼각형 메시의 문제점 중 하나는 아티스트가 처음에 메시를 만들 때 얼마만큼 테셀레이션할 지가 고정된다는 점이다. 고정된 테셀레이션을 쓰면 물체의 윤곽이 거칠게 보일 수 있다.

 

기하 셰이더를 사용하여 LOD를 구현하는 방법

https://lemonyun.tistory.com/53

 

12. 기하 셰이더

기하 셰이더는 기본도형을 입력받는다. 기본도형 마다 기하 셰이더가 실행된다. 기하 셰이더는 기하구조를 새로 생성하거나 폐기할 수 있다. 기하 셰이더에서 나오는 정점 위치들은 반드시 동

lemonyun.tistory.com

GPU상의 동적 LOD를 덮개 셰이더에서 구현하는 방법

https://lemonyun.tistory.com/55?category=1020933 

 

14. 테셀레이션 단계들

테셀레이션을 사용하는 이유 1. GPU상의 동적 LOD 2. 효율적인 물리 및 애니메이션 계산 ㄴ 물리와 애니메이션을 저다각형 메시에 대해 수행하고, 그 저다각형 메시를 테셀레이션해서 고다각형 버

lemonyun.tistory.com

 

10.1.1.3 삼각형 메시 만들기

감기 순서

ㄴ 감기 순서에 따라 전면 삼각형과 후면 삼각형을 구분하기 때문에 감기 순서를 혼동하지 않도록 주의해야 한다.

 

삼각형 리스트

ㄴ 메시를 이루는 각 삼각형의 정점을 3개씩 묶어 리스트로 나타내는 방식 (리스트의 크기는 삼각형 개수 * 3)

 

인덱스 삼각형 리스트

ㄴ 삼각형 리스트를 사용하면 중복되는 정점이 있을 수 있기 때문에 메모리가 낭비될 수 있다. 정점 버퍼와 인덱스 버퍼

 

스트립(strip)과 팬(fan)

ㄴ 인덱스 버퍼를 사용할 필요가 없으면서 정점의 중복을 줄여주는 효과가 있다.

정점 캐시 최적화

 

스트립과 팬을 쓰는 이유는 GPU가 비디오 RAM을 접근할 때 캐시 일관성을 향상시킬 수 있기 때문이다.

오프라인 기하 형상 처리 도구인 정점 캐시 최적화 도구를 사용하면 정점의 캐시 재사용성이 최대가 되도록 삼각형을 재배열해준다. 이는 인덱스 삼각형 리스트를 사용하더라도 캐시 최적화 효과를 얻을 수 있게 해준다.

 

10.1.1.4 모델 공간

삼각형 메시의 위치 벡터들을 나타낼 때는 대개 사용하기 편한 지역 좌표계 (local space) 를 기준으로 삼는다. 

 

10.1.1.5 월드 공간과 메시 인스턴스

각각의 메시들은 월드 공간 (world space)이라고 불리는 공통 좌표계를 기준으로 자리를 잡고 방향을 정해 완전한 장면을 구성한다. 한 장면에서 메시 하나가 여러 번 등장할 수 있는데 이 같은 물체를 메시 인스턴스라고 부른다.

메시 인스턴스에는 공통 메시 데이터에 대한 참조, 로컬 공간에서 월드 공간으로 변환하는 행렬 (월드 행렬 : world matrix) 이 포함되어 있다.

 

메시를 월드 공간으로 변환할 때, 메시의 정점에만 월드 행렬을 곱하는 것이 아니라 법선 벡터에도 곱해줘야 한다. 월드 행렬에 스케일이나 전단 변환이 없는 경우에는 그냥 곱하면 된다.

10.1.2 표면의 시각적 속성

난반사 색상(diffuse color), 반사율, 질감, 투명도, 굴절 정도 같은 표면 속성을 정의할 수 있다.

포토리얼리즘을 추구하는 이미지를 렌더링할 때 가장 중요한 점은 물체에 반응하는 빛의 작용을 제대로 처리하는 것이다. 그렇기 때문에 렌더링 엔지니어는 빛의 원리와 전달 방식, 그리고 가상 카메라에서 빛을 감지하고 이것을 스크린의 색으로 변환하는 방식 등을 잘 알고 있어야 한다.

https://lemonyun.tistory.com/49

 

8. 조명

8.1 빛과 재질의 상호작용 조명을 사용할 때에는 정점 색상들을 직접 지정하지 않음. 표면의 재질들과 표면에 비출 빛들을 지정하고 조명 방정식을 적용해서 정점 색상이 결정되게 한다. 국소 모

lemonyun.tistory.com

10.1.2.1 빛과 색에 대한 기초

빛과 물체의 상호작용

ㄴ 흡수된다.

ㄴ 반사된다.

ㄴ 물체를 통과한다. (= 굴절되어 통과한다)

ㄴ 매우 가는 틈새를 통과할 때 회절된다.

 

포토리얼리즘을 추구하는 대부분의 렌더링 엔진은 앞의 세 가지 요소를 모두 고려한다. 회절은 대부분의 경우 눈에 드러나지 않아 구현하지 않는 경우가 많다.

 

10.1.2.2 정점 속성

일반적인 삼각형 메시는 다음의 속성들 중 일부 혹은 전부를 각 정점에 담고 있다.

ㄴ 위치 벡터

ㄴ 정점 법선

ㄴ 정점 탄젠트와 바이탄젠트 - 정점 법선과 각각 수직이며 서로 수직이어서 3개의 벡터는 탄젠트 공간의 좌표축을 나타낸다. 탄젠트 공간은 다양한 픽셀 단위 조명 계산에 이용되며, 여기에는 법선 매핑과 환경 매핑이 해당된다.

ㄴ 난반사 색

ㄴ 정반사 색

ㄴ 텍스처 좌표

ㄴ 스키닝 가중치

 

10.1.2.3 정점 형식

정점 속성을 저장할 때는 보통 C의 구조체나 C++ 클래스 등의 자료 구조를 사용한다. 이 같은 자료 구조의 레이아웃을 정점 형식(vertex format) 이라고 한다. 메시 종류마다 다른 속성을 조합해 쓰기 때문에 각기 다른 정점 형식이 필요하다.

// 1. 가장 단순한 정점 - 위치만 있다.
// 
// z-프리패스, 카툰 렌더링의 실루엣 경계 검출, 그림자 볼륨 밀어내기 등에 유용하다.
// 
struct Vertex1P 
{
    Vector3 m_p; // 위치
}

// 2. 흔히 쓰이는 정점 형식, 정점 법선과 텍스처 좌표 한 벌을 갖는다.
//
struct Vertex1P1NiUV
{
    Vector3 m_p; // 위치
    Vector3 m_n; // 정점 법선
    F32 m_uv[2]; // (u, v) 텍스처 좌표
}

// 3. 스키닝에 쓰이는 정점, 위치, 난반사 색, 정반사 색 및 4개의 정점에 대한 가중치를 갖는다.
//
struct Vertex1P1D1S2UV4J
{
    Vector3 m_p; // 위치
    Color4 m_d; // 난반사 색과 투명도
    Color4 m_s; // 정반사 색
    F32 m_uv0[2]; // 첫 번째 텍스처 좌표
    F32 m_uv1[2]; // 두 번째 텍스처 좌표
    U8 m_k[4]; // 스키닝에 쓰이는 4개의 관절 인덱스
    F32 m_w[3]; // 3개의 가중치 (마지막 가중치는 1 - 나머지 가중치의 합)
}

DirectX 루나 책의 모든 예제에서는 하나의 메시가 하나의 머터리얼(난반사 색, 거칠기 계수, 매질의 반사율 속성)을 가지도록 설계되었기 때문에 정점 구조체에 굳이 머터리얼 속성을 넣지 않고 상수 버퍼를 통해 전달했다.

 

10.1.2.4 속성 보간

메시 표면 속성을 정점 단위가 아니라 픽셀 단위로 얻기 위해 정점 단위 속성 데이터를 선형 보간하여 픽셀 단위의 속성 데이터를 얻을 수 있다.

색상, 텍스처 좌표, 정점 법선과 같은 정점 속성 정보들을 보간할 수 있다.

 

10.1.2.5 텍스처

텍스처의 기본 단위는 텍셀이라고 부른다.

 

텍스처 종류

ㄴ 난반사 맵 (= 알베도 맵) : 메시의 벽지 역할을 한다. 텍셀에 표면의 난반사 색(벡터)를 담은 텍스처

ㄴ 법선 맵 : 텍셀의 단위 법선 벡터를 담은 텍스처

ㄴ 환경 맵 : 물체에 의해 반사된 주변의 환경을 물체에 입히기 위해 주변의 환경 이미지를 담은 텍스처

ㄴ 글로스 맵 : 각 텍셀이 얼마나 반짝이는지를 담은 텍스처

 

텍스처 좌표

ㄴ 2차원 좌표계 (u, v), 값의 범위는 (0,  0) ~ (1, 1)이다.

 

텍스처 주소 지정 방식

텍스처 좌표의 정의역 [0, 1] 바깥의 좌표가 주어졌을 때의 처리 방식

1. 순환 (wrap)

2. 테두리 색상 (border color)

3. 한정 (clamp)

4. 반사 (mirror)

 

텍스처 형식

요즘의 그래픽 카드와 그래픽 API들은 압축 텍스처를 지원한다.

DirectX는 DXT라고 알려진 압축 형식들을 지원한다.

압축 텍스처는 압축하지 않은 텍스처에 비해 메모리를 작게 사용하고 렌더링도 빠르다.(캐시 성능에 유리한 메모리 접근 패턴) 하지만 상황에 따라 텍스처가 이상하게 보이는 경우도 있으므로 잘 써야 한다.

 

밉맵과 필터링

https://lemonyun.tistory.com/50

 

9. 텍스처 적용

9.2 텍스처 좌표 법선 벡터와 마찬가지로 삼각형의 정점마다 텍스처 좌표를 지정해주면 보간에 의해 삼각형의 모든 점마다 그에 대응되는 텍스처 좌표가 결정된다. 9.3 텍스처 자료 원본 DDS (Direct

lemonyun.tistory.com

 

10.1.2.6 재질

재질 (Material)이란 메시의 시각적인 속성을 통틀어 일컫는 용어다. 메시 표면에 매핑되는 텍스처를 비롯한 하이레벨 속성들 (셰이더 프로그램, 셰이더에 들어갈 입력 인자들, 그래픽 가속 하드웨어를 제어하는데 쓰이는 인자) 이 포함된다.

3차원 모델은 일반적으로 여러 개의 재질을 사용한다. 이런 이유 때문에 메시 하나를 여러 개의 하부 메시로 나눠 각각 한 개의 재질에 연결하는 경우가 많다.

10.1.3 조명의 기본

10.1.3.1 지역 조명과 전역 조명 모델

빛 - 표면, 빛 - 공간 간의 상호작용에 관한 수학적 모델을 빛 수송 모델(Light Transport Model) 이라고 한다.

1. 지역 조명 모델

ㄴ 빛이 방출돼 물체 하나에만 반사된 후 바로 가상 카메라의 상 표면에 맺히는 직접 조명만 계산에 넣는 모델

 

2. 전역 조명 모델

ㄴ 간접 조명(빛이 여러 표면에 여러 번 반사해서 카메라에 도달하는 빛)을 고려하는 조명 모델

 

10.1.3.2 퐁 조명 모델

게임 렌더링 엔진들이 가장 흔히 사용하는 지역 조명 모델은 퐁(Phong) 반사 모델이다.

이 모델에서는 표면에서 반사되는 빛은 다음과 같은 세 가지 항의 합으로 표현된다.

ambient(환경, 주변)광 + diffuse(난반사, 분산)광 + sepcular(정반사, 반영)광

 

10.1.3.3 광원 모델링

정적 조명

ㄴ 미리 계산해 놓은 것을 그대로 쓰는 방식, 오프라인에 조명을 계산하는 것이 바람직함

ㄴ 조명 정보를 난반사 텍스처에 직접 입히는 방법은 좋지 않은 방법이다. 난반사 텍스처 맵은 장면의 다양한 곳에 반복적으로 사용되는 경우가 많기 때문이다.

ㄴ 광원마다 조명 맵을 생성하고 영향 범위 안에 들어오는 모든 물체에 이를 적용하는 방식을 쓴다.

 

환경광 광원 (주변광, Ambient Lights)

ㄴ 환경광 광원의 강도와 색은 월드 내 지역마다 다를 수 있다.

 

방향 광원 (Directional Lights)

ㄴ 태양에서 나오는 빛과 같이 무한히 먼 거리에서 오는 광원을 모델링한다.

ㄴ 빛의 색 C와 방향 벡터 L로 나타낸다.

 

점광 (Omni-Directional Lights)

ㄴ 분명한 위치가 있고 모든 방향으로 균등한 빛을 내는 광원을 모델링한다. 렌더링 엔진은 점 광원의 범위 안에 들어가는 표면에만 조명을 적용한다.

ㄴ 위치 P, 빛의 색 / 강도 C, 최대 범위 r로 나타낸다.

 

점적광 (Spot Lights)

ㄴ 빛의 안쪽 범위와 바깥 범위를 나타내는 원뿔 두 개를 사용한다.

ㄴ 위치 P, 안쪽 원뿔의 각도, 바깥 원뿔의 각도, 빛의 색 C, 중심 방향 벡터 L, 최대 반지름 r로 나타낸다.

 

면적 광원 

ㄴ 직접 모델링하는 대신 그림자를 여러개 만든 후 블랜딩하거나 그림자의 날카로운 경계를 둔하게 만드는 방법도 있다.

 

발광체

ㄴ 표면 자체가 빛을 내는 광원인 경우

ㄴ 발광하는 표면은 발광 텍스처 맵(emissive texture map, 주변 환경이 어떻든 온전한 강도의 색을 가지는 텍스처)으로 모델링할 수 있다.

10.1.4 가상 카메라

10.1.4.1 뷰 공간

DirectX는 왼손 좌표계를 사용하기 때문에 다음과 같은 그림처럼 된다.

왼손 좌표계의 뷰 공간

월드 공간의 정점을 뷰 공간의 정점으로 변환하는 행렬을 뷰 행렬이라고 부른다.

뷰 행렬은 카메라의 로컬 행렬의 역행렬이다.

메시 인스턴스를 렌더링하기 전에 월드 행렬과 뷰 행렬을 미리 결합해두는 경우가 많다.

이는 정점을 모델 공간에서 뷰 공간으로 변환할 때 행렬 곱셈을 한 번만 해도 되게 해준다.

 

10.1.4.2 투영

뷰 공간을 직교 투영, 원근 투영할 수 있다.

투영 행렬은 뷰 공간의 정점을 동차 클립 공간(homogeneous clip space)이라고 불리는 좌표계로 변환한다.

 

원근 투영은 길이가 보존되지 않는다. 실제 카메라가 찍는 것과 같이 멀리있는 물체는 작게 보인다.

직교 투영은 길이가 보존된다. 주로 3차원 모델 편집이나 게임 레벨 편집 시에 사용한다.

 

10.1.4.3 뷰 볼륨과 절두체

카메라가 볼 수 있는 영역을 뷰 볼륨이라고 한다. 뷰 볼륨은 평면 6개로 정의한다. 

뷰 볼륨을 이루는 여섯 평면들은 원소가 4개인 벡터를 6개 써서 표현할 수 있다.

 

원근 투영을 사용해 장면을 렌더링하는 경우 뷰 볼륨은 절두체가 되고 직교 투영의 경우는 그냥 직육면체가 된다.

 

10.1.4.4 투영과 동차 클립 공간

동차 좌표계 벡터를 3차원 좌표계(정규화된 장치 좌표 공간 : NDC) 로 바꾸려면 x, y, z 성분을 w 성분으로 나눠야 한다. (원근 나누기)

동차 좌표의 w 성분은 뷰 공간 z좌표와 같다. (DirectX 기준) 

 

원근 보정 정점 속성 보간

속성 보간은 스크린 공간에서 수행하는데, 장면을 원근 투영한 후 렌더링할 때는 원근 단축 효과를 감안해야 한다.

ㄴ 두 정점을 보간할 때 두 정점의 속성값을 각 정점의 z 좌표(깊이)로 나눠야 한다.

 

10.1.4.5 스크린 공간과 화면 비율

스크린 공간

스크린 공간은 2차원 좌표계로, 좌표축 단위는 스크린 픽셀이다. 원점은 화면 왼쪽 위, x 축은 오른쪽 방향, y축은 아래 방향이다.

 

10.1.4.6 프레임 버퍼

렌더링한 최종 이미지는 프레임 버퍼라고 불리는 비트맵 컬러 버퍼에 저장된다. 픽셀의 색은 보통 RGBA8888형식을 사용한다.

디스플레이 하드웨어는 프레임 버퍼의 내용을 주기적으로 읽는다.

렌더링 엔진에는 프레임 버퍼가 최소 두 개 있다. 디스플레이 하드웨어가 하나를 읽는 동안 렌더링 엔진은 다른 버퍼를 업데이트 한다.(이중 버퍼링)

 

렌더 타겟

프레임 버퍼 외에도 깊이 버퍼, 스텐실 버퍼를 비롯해 중간 렌더링 결과를 저장하는 다양한 버퍼들이 있다.

 

10.1.4.7 삼각형 래스터화와 단편

삼각형을 화면에 그릴 때 삼각형이 걸쳐 있는 픽셀들을 채우는 과정을 래스터화라고 한다.

단편은 화면의 픽셀에 대응되는 삼각형의 일부 영역이다. 렌더링 파이프라인을 거치면서 버려지는 경우도 있고, 버려지지 않는다면 프레임 버퍼에 색이 기록된다.

단편은 몇 번의 테스트(깊이, 스텐실)를 통과한 후에 프레임 버퍼에 기록된다.

단편의 색은 프레임 버퍼에 기록되거나 기존에 있는 픽셀 색과 블렌딩된다.

 

10.1.4.8 차폐와 깊이 버퍼

그리는 순서에 관계없이 삼각형들이 제대로 가려지게 렌더링 엔진은 깊이 버퍼를 사용한다.

깊이 버퍼는 보통 프레임 버퍼와 같은 해상도를 갖고 각 픽셀에 대해 24비트 정수의 깊이 값과 8비트 스텐실 값을 묶어 픽셀당 32비트 포맷에 저장된다.

 

10.2 렌더링 파이프라인

렌더링 파이프라인의 각 단계는 다른 단계들과는 무관하게 독립적으로 동작하기 때문에 병렬화에 유리하다.

파이프라인의 한 단계(계산 셰이더)에서도 병렬화를 얻을 수 있다. 

 

파이프라인의 설계가 잘 되었다면 모든 단계가 동시에 동작하면서도 다른 단계가 끝나기를 오래 기다리고 있는 단계가 없어야 한다.

10.2.1 렌더링 파이프라인 개요

툴 단계 (오프라인)

ㄴ 기하 형상과 표면 속성을 정의한다.

 

자원 다듬기 단계 (오프라인)

ㄴ 기하 형상과 재질 데이터들을 가공해 엔진에서 즉시 사용할 수 있는 형태로 변환한다.

 

애플리케이션 단계 (CPU)

ㄴ 보여질 가능성이 있는 메시 인스턴스를 판별하고 이것들을 재질과 함께 그래픽 하드웨어에 보내 렌더링할 수 있게 한다.

 

기하 형상 처리 단계 (GPU)

ㄴ 정점을 변환하고 조명을 적용한 후 동차 클립 공간으로 투영한다. 부가적으로 기하 셰이더에서 삼각형들을 처리한 다음에 절두체 클리핑을 할 수도 있다.

 

래스터화 단계 (GPU)

ㄴ 삼각형을 쪼개고, 색을 결정하고 다양한 테스트(z-테스트, 알파 테스트, 스텐실 테스트)를 거친 후 마지막으로 프레임 버퍼에 렌더링한다.

 

10.2.1.1 렌더링 파이프라인이 처리하는 데이터 형식

툴과 자원 다듬기 단계 - 메시와 재질을 다룬다.

애플리케이션 단계 - 메시 인스턴스와 하부 메시 단위로 처리한다. (하부 메시는 재질 하나와 연결된다.)

기하 형상 단계 - 하부 메시들을 정점 단위로 쪼개어 처리한다. (병렬로 처리된다.) 정점으로 삼각형을 구성한다.

래스터화 단계 - 삼각형을 단편으로 분해한다.

 

10.2.2 툴 단계

3DS 맥스, 마야등의 3차원 모델러를 사용하여 메시를 제작한다.

스키닝 메시를 만드는 경우 각 정점을 하나 이상의 뼈대 구조 관절에 연결시키는 작업이 필요하고, 관절들이 해당 정점에 미치는 영향을 나타내는 가중치도 같이 지정해야 한다.

아티스트가 재질에 쓰일 셰이더, 텍스처, 셰이더의 옵션, 인자를 지정하는 일들이 툴 단계에서 진행된다.

ㄴ 언리얼 엔진은 그래픽 셰이더 언어를 제공하는데 그래픽 언어로 만든 셰이더들은 나중에 렌더링 엔지니어가 손으로 최적화해야 하는 경우가 많다.

10.2.3 자원 다듬기 단계

자원 다듬기 단계는 그 자체로 파이프라인이므로 ACP(Asset Conditioning Pipeline)이라고 불리기도 한다.

3차원 모델은 기하 형상(정점 버퍼, 인덱스 버퍼), 재질, 텍스처, 때로는 뼈대 등이 모여 이루어진다.

 

기하 형상과 재질 데이터는 DCC 프로그램에서 뽑아내서 플랫폼 중립적인 중간 형식으로 저장하는 것이 보통이다. 이 데이터를 더 가공해 여러 개의 플랫폼 특화된 형식으로 변환하는데, 그 수는 엔진이 지원하는 플랫폼의 개수에 따라 달라진다. 

 

재질이나 셰이더의 요구 조건에 따라 ACP가 자원을 제작하는 방식이 바뀌기도 한다. 예를 들어 어떤 셰이더(환경 매핑을 위한 셰이더)는 정점 법선 외에도 탄젠트와 바이탄젠트 벡터를 필요로 하는 겨우가 있는데 ACP에서 이 같은 벡터들을 자동으로 만들게 할 수 있다.

 

장면 그래프를 사용하는 경우 이에 대한 계산도 ACP에서 하면 된다. 이 경우 정적 레벨 기하 형상들을 처리해 BSP 트리를 만든다.

 

미리 정적 조명을 계산하는 것도 포함한다. (라이트를 '굽는다'는 표현을 사용)

 

10.2.4 GPU의 간략한 역사

GPU의 주된 목적은 파이프라인의 처리량을 최대화하는 것이다. (거대한 규모의 병렬화)

그래픽과 관련되지 않은 목적 수행을 위한 GPU 프로그래밍은 GPGPU(General-purpose GPU)라고 한다.

10.2.5 GPU 파이프라인

GPU 파이프라인

https://lemonyun.tistory.com/46

 

5. 렌더링 파이프라인

5.3.2 128 비트 색상 R G B A 각각 32bit씩 부동소수점 표현 가능 XMVector 형식으로 색상을 표현할 수 있고 색상 연산을 수행할 때 SIMD의 혜택을 받을 수 있음 5.3.3 32 비트 색상 XMCOLOR 구조체는 32bit 색상..

lemonyun.tistory.com

10.2.5.1 정점 셰이더(프로그래밍 가능)

모델 공간으로 들어온 정점을 뷰 공간으로 변환하는 코드를 작성할 수 있다.

 

10.2.5.2 지오메트리 셰이더(선택적, 프로그래밍 가능)

입력으로 기본 단위(삼각형, 선분, 점)를 culling하거나 수정하거나 새로운 기본 단위를 만들 수도 있다.

사용 예시

ㄴ 그림자 볼륨 밀어내기

ㄴ 큐브 맵의 여섯 면을 렌더링하기

ㄴ 실루엣 모서리를 따라 fur fin 밀어내기 (?)

ㄴ 파티클 효과에서 점 데이터로 쿼드 생성하기

ㄴ 동적 테셀레이션

ㄴ 번개 효과를 위한 선분 프랙탈 분할

ㄴ 옷감 시뮬레이션

 

10.2.5.3 스트림 출력

어떤 GPU는 파이프라인에서 지금까지 처리된 데이터를 다시 메모리에 저장하는 기능을 지원한다.

 

10.2.5.4 클리핑(고정, 일부는 설정)

절두체에 걸치는 삼각형의 일부를 잘라낸다. 클리핑하는 과정은 먼저 절두체 밖에 있는 정점들을 알아내고, 삼각형의 모서리가 절두체의 평면과 교차하는 지점을 찾는다. 교차하는 지점들이 새로 정점이 되고, 이들을 통해 하나 이상의 새로운 삼각형을 이룬다. (고정)

절두체 평면 외에도 별도의 클리핑 평면을 더할 수 있다. (설정)

 

10.2.5.5 스크린 매핑(고정)

동차 클립 공간에 있는 정점들을 스크린 공간으로 이동시킴

 

10.2.5.6 삼각형 셋업(고정)

삼각형을 단편으로 쪼개는 과정을 효율적으로 처리하기 위해 래스터화 하드웨어를 초기화 한다.

 

10.2.5.7 삼각형 순회(고정)

각 삼각형을 삼각형 순회 단계에 의해 단편들로 쪼갠다.(래스터화) 보통 픽셀 하나당 단편 하나를 만들지만 다중 샘플 안티엘리어싱(MSAA)를 하는 경우에 픽셀 하나에 여러 단편들을 만든다.

픽셀 셰이더를 위한 단편 속성을 만들기 위해 정점 속성을 보간한다.

 

10.2.5.8 이른 z-테스트 (하드웨어에 따라 다름)

상당수의 그래픽 카드는 파이프라인의 현 단계에서 단편의 깊이를 체크하는 기능을 지원한다. 프레임 버퍼에 있는 픽셀(이미 그려진 단편?)에 가려지는 경우 단편을 버릴수 있기 때문에 버려진 단편에 대해 픽셀 셰이더를 거치지 않게 되어 시간을 절약할 수 있다.

 

10.2.5.9 픽셀 셰이더 (프로그래밍 가능)

단편 단위 속성들의 모음을 입력으로 받는다.

단편의 색을 최종 결정한다.

단편을 버릴 수도 있다.

 

10.2.5.10 합치기 / 래스터 작업 단계 (프로그래밍할 수는 없지만 설정 가능)

깊이 테스트, 알파 테스트, 스텐실 테스트가 이 단계이다.

 

10.2.6 프로그래밍 가능한 셰이더

10.2.6.1 메모리 접근

GPU가 구현하는 데이터 처리 파이프라인 특성상 RAM에 접근하는 일은 세심하게 제어된다.

셰이더 프로그램은 메모리를 직접 읽거나 쓸 수 없다. 대신에 레지스터와 텍스처 맵을 통해 메모리에 접근할 수 있다.

 

1. 셰이더 레지스터

GPU 레지스터는 모두 128비트 SIMD 형식이다.

레지스터 하나로 32비트 자료를 4개 담은 4차원 벡터 하나를 표현할 수 있다.

행렬은 서너개의 레지스터를 묶어서 표현할 수 있다.

 

입력 레지스터

ㄴ 셰이더가 입력 데이터를 받는 주요한 수단

ㄴ GPU는 셰이더를 호출하기 전에 비디오 RAM에서 데이터를 입력 레지스터에 복사한다.

 

상수 레지스터

ㄴ 애플리케이션이 값을 지정하여 셰이더에 보낸다.

ㄴ 셰이더에서 필요로하지만 정점 속성으로 제공되지 않는 온갖 매개변수 (모델-뷰 행렬, 투영 행렬, 조명 매개변수 등)

 

임시 레지스터

ㄴ 셰이더 프로그램 안에서 사용할 수 있고 보통 중간 값을 저장하는데 사용한다.

 

출력 레지스터

ㄴ 셰이더의 출력물은 출력 레지스터에 저장된다.

ㄴ 셰이더 프로그램이 끝난 후에 GPU는 출력 레지스터 값을 다시 RAM에 저장해 다음 파이프라인 단계로 넘어갈 수 있게 한다. (보통은 캐시에 저장한다.)

 

2. 텍스처

ㄴ 셰이더는 텍스처를 읽기 전용 데이터로 직접 읽을 수 있다.

ㄴ 텍스처 데이터는 메모리 주소가 아닌 텍스처 좌표 (u, v)로 접근한다.

ㄴ GPU의 텍스처 샘플러가 자동으로 필터링하게 된다.(인접한 밉맵 레벨을 알아서 가져와 블렌딩한다.)

ㄴ 셰이더가 텍스처 맵에 데이터를 기록하고 싶으면 간접적인 방식을 통해야 한다. 오프스크린 프레임 버퍼에 장면을 렌더링하고 이를 다음 렌더링 패스에서 텍스처 맵으로 인식하게 하는 방식이다. (텍스처에 렌더링)

 

10.2.6.2 하이레벨 셰이더 언어 문법

셰이더 프로그램은 레지스터와 텍스처에만 접근할 수 있기 때문에 하이레벨 셰이더 언어에서 선언하는 struct와 변수는 셰이더 컴파일러가 레지스터에 직접 연결시킨다.

 

시맨틱 

ㄴ 변수나 struct 멤버 뒤에 콜론을 붙이고 시맨틱이라는 키워드를 붙일 수 있다.

ㄴ 시맨틱은 셰이더 컴파일러에게 해당 변수나 데이터 멤버를 특정한 정점 혹은 단편 속성과 연결하게 알려준다.

입력 값과 출력 값

ㄴ 어떤 변수나 struct가 입력 레지스터에 연결될지 출력 레지스터에 연결될지를 컴파일러가 판단할 때는 어떤 문맥으로 쓰였는지를 본다. 셰이더 프로그램의 메인 함수에 인자로 전달된 변수인 경우에는 입력 값이라고 가정하고, 리턴 값은 출력 값으로 판단한다.

 

uniform 선언

ㄴ 애플리케이션으로부터 상수 레지스터로 전달되는 데이터에 접근하려면 변수를 선언하면서 uniform 키워드를 사용하면 된다. 

 

10.2.6.3 이펙트 파일

셰이더 프로그램을 엮어 완전한 시각 효과를 만들려면 이펙트 파일이라는 파일 형식을 이용해야 한다.

ㄴ 전역 공간에는 struct 값, 셰이더 프로그램들과 전역 변수들이 정의된다.

ㄴ 하나 혹은 그 이상의 테크닉을 정의한다. 테크닉은 특정한 시각 효과가 렌더링되는 한 방식을 나타낸다.

ㄴ 각 테크닉 안에 한 개 혹은 그 이상의 패스(정점, 기하, 픽셀 셰이더의 main 함수 등) 를 정의한다.

10.2.7 안티엘리어싱

10.2.7.1 전체 화면 안티엘리어싱 (Full-Screen Antialiasing, FSAA)

ㄴ 수퍼 샘플링 안티앨리어싱이라고도 불린다.

ㄴ 가로 세로 두배 큰 (4배) 프레임 버퍼에 렌더링되기 때문에 메모리 사용량이 4배가 되고 GPU 사용량도 4배가 된다.(픽셀 셰이더를 4배 더 사용)

ㄴ 프레임 렌더링이 끝나면 축소 샘플링하여 원래대로 되돌린다.

 

10.2.7.2 멀티 샘플링 안티엘리어싱 (MultiSampled Antialiasing, MSAA)

ㄴ 픽셀 하나에 대해 범위 테스트와 깊이 테스트는 픽셀의 슈퍼 샘플이라고 알려진 N개의 점에서 실행되지만 픽셀 셰이더는  한 번만 실행된다

10.2.8 애플리케이션 단계

1. 가시성 결정

ㄴ 보이는 물체들만 CPU에 넘겨야 한다.

 

2. 기하 형상을 GPU에 제출해 렌더링한다.

ㄴ 하부 메시와 재질로 이루어진 쌍을 GPU에 제출할 때는 렌더링 함수를 호출하거나 GPU 명령 리스트를 직접 조합한다.

 

3. 셰이더 전달 인자와 렌더 상태를 제어한다.

ㄴ 셰이더에 상수 레지스터나 버퍼로 전달되는 인자들을 설정한다.

 

10.2.8.1 가시성 결정

절두체 선별

https://lemonyun.tistory.com/57

 

16. 인스턴싱과 절두체 선별

인스턴싱 : 한 장면에서 같은 물체를 여러 번 그리는 것, 성능을 크게 향상할 수 있다. 절두체 선별 : 시야 절두체 바깥에 있는 일단의 삼각형들을 간단한 판정으로 골라내서 기각하는 기법 16.1

lemonyun.tistory.com

물체의 경계 구 와 절두체의 교차 판정(절두체의 6평면 안쪽에 있는지)을 평면 방정식을 사용하여 계산한다.

장면 그래프를 사용하면 절두체 선별 과정이 더욱 효율적이다.

 

차폐 선별

다른 물체에 완전히 가려지는 물체를 보이는 것들의 리스트에서 제거하는 것을 차폐 선별(occlusion culling) 이라고 한다.

 

잠재적 가시 그룹 (PVS, potentially visible set)

ㄴ 규모가 큰 환경에서는 미리 계산된 잠재적 가시 그룹을 통해 대략적인 차폐 선별을 할 수 있다.

ㄴ 카메라가 바라보는 공간에서 보일만할 물체를 미리 리스트로 지정해 놓는 방법

 

포탈

ㄴ 게임 월드의 지역을 연결하는 창문이나 문 같은 구멍을 포탈이라고 한다.

ㄴ 카메라의 초점에서 포탈의 폴리곤 모서리들로 이어지는 평면들을 만들고 이것들로 이루어지는 절두체 모양의 입체 (포탈 볼륨)을 만든다.

 

차폐 볼륨 (안티 포탈)

ㄴ 포탈의 개념을 뒤집으면 어떤 물체에 가려서 완전히 보이지 않는 지역을 나타내는데 피라미드형 입체를 쓸 수 있다. 물체의 경계 모서리들을 지나는 평면들로 차폐 볼륨을 만든다. 더 멀리 있는 물체와 차폐 볼륨을 검사해 물체가 차폐 볼륨 안에 완전히 들어오는 경우에 물체를 그리지 않아도 된다.

 

포탈이 효과적인 상황은 밀폐된 실내 환경에서 상대적으로 적은 창문이나 문을 통해 방들이 연결돼 있는 환경을 렌더링하는 상황 (포탈이 전체 카메라 절두체에서 상대적으로 작은 비율만 차지하기 때문에 포탈 바깥에 있는 많은 물체들을 선별할 수 있다.

 

차폐 볼륨이 효과적인 상황은 넓은 실외 환경에서 가까운 물체들이 카메라 절두체를 상당부분 가리는 상황 (차폐 볼륨이 카메라의 절두체에서 상대적으로 큰 부분을 차지하기 때문에 많은 물체들을 선별할 수 있다.)

 

10.2.8.2 기본 단위 제출

렌더 상태

GPU 파이프라인 내 설정 가능한 모든 인자들을 모아서 하드웨어 상태, 혹은 렌더 상태라고 한다.

ㄴ 월드 뷰 행렬

ㄴ 조명 방향 벡터

ㄴ 텍스처 연결 (재질과 셰이더에 어떤 텍스처를 쓸지)

ㄴ 텍스처 주소 지정 방식 및 필터링 모드

ㄴ 텍스처 스크롤이나 기타 애니메이션 이펙트를 위한 시간 축

ㄴ z-테스트 여부

ㄴ 알파 블렌딩 옵션

등..

 

상태 누수

기본 단위를 제출하기 전에 렌더 상태를 재설정해야 하는데, 이때 빠트린 것이 있다면 이전 기본 단위에 쓰였던 상태가 누수되어 다음 상태를 그리는데 영향을 준다. 재설정을 꼼꼼히 해야 한다.

 

10.2.8.3 기하 형상 정렬

렌더 상태 설정은 전역이다. 즉 전체 GPU에 영향을 미친다. 렌더 상태를 변경하려면 먼저 GPU의 파이프라인을 모두 비우고 새로운 설정을 적용해야 한다. 그러므로 렌더 상태를 가능한 적게 바꾸도록 해야 한다.

 

재질에 따라 기하 형상을 정렬하면 렌더링 성능이 저하될 수 있다. 중복 그리기(한 픽셀이 겹치는 여러 삼각형에 의해 여러번 채워지는 경우) 가 증가하기 때문이다.

 

이른 z-테스트는 시간이 오래 걸리는 픽셀 셰이더가 실행되기 전에 가려지는 단편들을 버리는 용도로 만든 것이다. 이른 z 테스트를 최대한 활용하려면 삼각형을 앞에서 뒤로 그려야 한다.

 

해결사 z-프리패스

GPU에는 픽셀 셰이더를 끄고 z-버퍼의 내용만 업데이트하는 일반 렌더링보다 몇 배 빠른 렌더링 모드가 있는데 

첫 번째 렌더 단계에서 z-버퍼의 내용만 업데이트 (기하 형상들을 앞에서 뒤로 그린다.)

두 번째 렌더 단계에서 색 정보로 프레임 버퍼를 채운다.

 

10.2.8.4 장면 그래프

요즘에는 게임 세계가 매우 큰 경우가 많아서 게임 세계에 존재하는 모든 물체에 대해 절두체 선별을 하는 것은 시간 낭비이다. 따라서 장면 내 모든 기하 형상들을 관리하면서 자세한 절두체 선별 과정에 들어가기 전에 카메라 절두체 근처에 있지 않는 물체들을 빠르게 가려낼 자료구조가 필요하다. 이런 자료 구조를 장면 그래프라고 부른다.

 

쿼드 트리, 옥트리, BSP 트리, kd 트리, 공간 해시 기법이 있다.

 

쿼드트리와 옥트리

재귀적 방식으로 공간을 분할한다.

렌더링 엔진에서 사용하는 쿼드트리는 메시 인스턴스나 하부 지형, 정적인 메시의 개별 삼각형 등 렌더링 가능한 기본 단위들을 저장해서 효율적인 절두체 추려내기를 하는데 쓰인다. 렌더링 가능한 기본 단위는 트리의 리프에 저장되고, 각 리프마다 저장하는 기본 단위의 개수를 균등하게 만드는 것이 일반적이다.

옥트리는 쿼드트리를 3차원에 적용한 것

 

BSP 트리 (Binary Space Partitioning, 이진 공간 분할)

공간을 반으로 나누는데, 나누고 난 절반이 특정한 조건을 충족할 때까지 반복한다.

BSP 트리는 충돌 검출, CSG(Constructive Solid Geometry) 등 여러 곳에 쓰이는데, 가장 활용 빈도가 높은 곳은 3차원 그래픽의 절두체 선별과 기하 형상 정렬이다.

BSP 트리 개념을 k-차원으로 일반화한 것이 kd-트리이다.

 

10.2.8.5 장면 그래프 선택

장면을 렌더링할 때 필요한 것이 무엇인지를 분명히 이해하고 있어야 게임의 장면에 맞는 자료구조를 사용할 수 있다.

 

밀폐된 실내 환경을 주 배경으로 하는 게임이라면 BSP 트리나 포털 시스템이 유용하다.

 

평탄한 실외를 배경으로 하고 주로 위에서 내려다보는 장면이 많은 게임의 경우 (RTS) 단순한 쿼드트리만 써도 좋다.

 

실외 장면을 평지에서 보는 경우 별도의 선별 방식이 필요할 수도 있다. 밀도가 높은 장면의 경우 가리는 물체가 많기 때문에 차폐 볼륨 시스템을 쓰면 도움이 될 수 있다.

 

장면 그래프를 고를 떄 최선의 방식은 렌더링 엔진의 성능을 실제로 측정해 보고 얻은 구체적 데이터를 바탕으로 고르는 방식이다.

 

10.3 고급 조명 기법과 전역 조명

10.3.1 이미지 기반 조명

고급 조명 및 셰이더 기법 중에는 이미지 데이터(2차원 텍스처 맵)를 활용하는 것들이 많다.

 

10.3.1.1 법선 매핑

법선 맵은 각 텍셀의 표면 법선 방향을 나타내는 벡터를 담고 있다.

 

10.3.1.2 높이 맵: 범프, 시차, 변위 매핑

시차 차폐 매핑

ㄴ 높이 맵의 정보를 사용하여 텍스처 좌표를 조정한다.

 

변위 매핑

ㄴ 실제로 테셀레이션을 하여 정점을 만들고 위치를 조정한다. 실제 기하 형태로 만들어지기 때문에 자체적으로 가리기와 그림자를 생성한다.

 

10.3.1.3 정반사(광택) 맵

정반사 맵은 각 텍셀에 정반사도를 저장한 텍스처이다.

대다수 표면의 광택은 균등하지 않기 때문에 정교한 정반사도를 저장하고자 할 때 사용할 수 있다.

 

10.3.1.4 환경 매핑

환경 맵은 물체를 둘러싼 전반적인 조명 환경을 나타낸 것이다. 큰 비용을 들이지 않고도 조명 환경의 반사를 구현하는데 쓰인다.

구형 환경 맵이나 큐브 환경 맵의 형식을 주로 사용한다.

 

10.3.1.5 3차원 텍스처

오늘날의 그래픽 하드웨어는 3차원 텍스처를 지원한다. 3차원 좌표(u, v, w)가 주어지면 GPU가 알아서 3차원 텍스처의 주소를 찾고 필터링한다.

 

10.3.2 HDR 조명

프레임 버퍼의 색 채널이 0과 1사이 범위보다 큰 값을 다루기 위해 HDR 조명을 사용한다.

10.3.3 전역 조명

표면끼리 가릴 때 생기는 그림자, 반사 초곡면 효과을 표현

어떤 물체의 색이 주변 물체에 배어 나오는 현상을 표현

 

10.3.3.1 그림자 렌더링

가장 널리 쓰이는 그림자 렌더링 기법은 그림자 볼륨과 그림자 맵이다.

그림자 볼륨

ㄴ 그림자를 내는 광원의 위치에서 그림자를 지게하는 물체들을 바라보고 물체의 외곽선 모서리들을 구한다.

ㄴ 스텐실 버퍼를 사용하여 그림자를 렌더링한다.

https://lemonyun.tistory.com/52

 

11. 스텐실 적용

스텐실 버퍼는 후면 버퍼, 깊이 버퍼와 해상도가 같다. 스텐실 버퍼는 특정 픽셀 단편들이 후면 버퍼에 기록되지 못하게 하는 역할을 한다. PSO 에 D3D12_DEPTH_STENCIL_DESC 구조체를 채워 설정할 수 있

lemonyun.tistory.com

 

그림자 맵

그림자 맵 기법은 카메라의 시점이 아닌 광원의 시점에서 수행하는 단편 단위 깊이 테스트다.

장면을 두 단계로 나눠 렌더링한다.

 

첫째 단계는 광원의 시점에서 장면을 렌더링한 후 깊이 버퍼의 내용을 따로 저장해서 그림자 맵 텍스처를 만든다. 그림자 맵을 렌더링할 때는 하드웨어에 있는 초고속 z-only 모드를 이용한다.

 

둘째 단계는 장면을 통상적으로 렌더링하고 그림자 맵을 사용해 각 단편이 그림자 안에 들어가는지를 판별한다.

ㄴ 정점들을 광원 공간으로 변환하고, 광원 공간의 (x,y) 좌표를 그림자 맵의 텍스처 좌표 (u,v)로 변환한 뒤 광원 공간 z 좌표와 그림자 맵에 저장된 깊이 정보와 비교한다.

 

10.3.3.2 환경광 차폐(Ambient occlusion)

환경광 차폐는 원통형 파이프와 같이 빛이 도달하기 어려운 부분(원통의 내부)이 있는 물체의 컨택트 섀도우(contact shadow) (환경광으로 장면을 조명할 때 생기는 약한 그림자)를 모델링하는 기법이다.

 

정적인 물체에 대해서는 오프라인에 미리 계산할 수도 있다. 환경광 차폐는 시선 방향과 빛의 입사각과는 무관하기 때문이다.

https://lemonyun.tistory.com/62

 

21. 주변광 차폐

21.1 반직선 투사를 통한 주변광 차폐 3차원 모형을 주변광 항으로만 조명하면 물체 전체에 하나의 색이 고르게 입혀진 모습이 나옴 주변이 얼마나 가려졌는지를 추정하여 차폐도(가려진 정도)를

lemonyun.tistory.com

 

10.3.3.3 반사

1. 반짝이는 물체에 반사되는 대강의 주변 환경을 반사할 때는 환경 맵을 사용한다.

2. 거울 등의 평평한 표면에 직접 반사되는 것을 구현할 때는 카메라의 위치를 반사 표면에 대칭시키고 그 위치에서 장면을 텍스처에 렌더링한다. 그런 후 두 번째 패스에서 이 텍스처를 반사 표면에 입힌다. (스텐실 버퍼를 사용한 스텐실 판정 기법을 사용한다.)

(카메라를 대칭시키는게 아니라 메시를 반사하는 방법도 있다. DirectX 루나책 11장 평면 거울 구현할 때 이렇게 했었다.) 

 

10.3.3.4 초곡면 효과

초곡면 효과란 물이나 광택 있는 금속 등에서 발생하는 강렬한 반사 혹은 산란으로, 매우 밝은 정반사 하이라이트다.

초곡면 효과는 어느 정도 랜덤한 하이라이트를 담고 있는 텍스처를 원하는 표면에 투영하는 식으로 구현한다.

 

10.3.3.5 표면하 산란

표면하 산란 - 빛이 표면의 한 점으로 들어가면 표면 아래에서 산란한 후 다른 지점에서 밖으로 나오는데, 이것을 표면하 산란(Subsurface scattering) 이라고 부른다. 사람의 피부나 밀랍, 대리석 조각상이 따스한 질감을 보이는 이유가 이 때문이다.

 

10.3.3.6 PRT (PreComputed Radiance Transfer)

입사광이 표면에 어떻게 작용하는지를 모든 방향에서 미리 계산하고 저장하는 방식

 

10.3.4 지연 렌더링

통상적인 삼각형 래스터화 기반 렌더링에서 모든 조명과 셰이딩 계산은 월드곤간, 뷰 공간 또는 탄젠트 공간의 삼각형 단편들을 가지고 한다. 이 방법은 삼각형의 정점들에 대한 여러 연산을 기껏 해놓고도 래스터화 단계에서 삼각형이 z-테스트를 통과하지 못해 버려질 수도 있기 때문에 비효율적일 수밖에 없다.

 

지연 렌더링에서 대부분의 조명 계산은 뷰 공간이 아니라 스크린 공간에서 수행한다. 픽셀 조명에 필요한 모든 정보는 G-버퍼라고 불리는 두꺼운 프레임 버퍼(여러 개의 프레임 버퍼)에 저장한다. 장면을 완전히 렌더링한 후 G-버퍼의 정보를 이용해 조명과 셰이딩 계산을 한다.

 

10.3.5 물리 기반 셰이딩

물리 기반 셰이딩 모델은 아티스트와 조명 전문가가 실세계에서 직관적이고 실세계와 같은 결과를 위해 셰이더 파라미터를 설정할 수 있게 하면서  실세계에서 빛이 이동하고 물질에 반응하는 방식을 거의 정확하게 구현한다.

 

10.4 시각 효과와 오버레이

시각 효과를 위해 렌더링 파이프라인 위에 여러가지 특수한 렌더링 시스템 계층을 올리는 경우가 많다.

 

10.4.1 파티클 효과

파티클 효과가 다른 렌더링 기하 형상들과 구분되는 주요한 특징

ㄴ 상대적으로 단순한 기하 형상들이 여러 개 모여 이루어진다.

ㄴ 기하 형상들은 항상 카메라를 향한다.(예시 : 빌보드), 쿼드의 표면 법선이 항상 카메라를 향하도록 엔진에서 따로 처리를 해야한다는 뜻이다.

ㄴ 재질이 거의 예외 없이 반투명하다. 그렇기 때문에 파티클 효과는 다른 대다수의 불투명한 물체들과는 달리 엄격한 렌더링 순서를 지켜야 하는 제약을 갖는다.

ㄴ 파티클은 보통 다양한 방식으로 애니메이션한다. 파티클의 위치, 방향, 크기(스케일), 텍스처 좌표, 셰이더 전달 인자 등은 프레임마다 달라진다. 이런 변화는 직접 만든 애니메이션 곡선으로 지정할 수도 있고, 절차적인 방식으로 정의할 수도 있다.

ㄴ 파티클은 계속해서 생겼다가 없어진다. 파티클 이미터는 월드에서 지정된 속도로 파티클들을 생성하는 논리적 단위다.

파티클은 지정된 평면에 부딪히거나 정해진 수명이 다헀을 때, 혹은 다른 조건이 충족되면 없어진다.

 

파티클 효과를 일반적인 삼각형 기하 형상과 셰이더를 조합해 만들 수도 있지만 이런 특징들 때문에 특수화된 파티클 효과 애니메이션 및 렌더링 시스템을 사용하는 경우가 대부분이다.

 

10.4.2 데칼

데칼은 기하 형상들의 표면에 씌우는 상대적으로 작은 기하 형상으로, 총탄 흔적, 발자국, 긁힌 흔적, 갈라진 흔적 등이 있다.

엔진에서 데칼을 구현할 때 가장 많이 쓰는 방법은 데칼을 사각형 영역으로 모델링한 후 화면에 일직선으로 투영하는 방식이다. (스크린 공간 -> 월드 공간)

이렇게 하면 3차원 공간에 직육면체 프리즘이 생긴다. 이 프리즘이 가장 먼저 교차하는 표면에 데칼이 입혀진다.

교차하는 기하 형상의 삼각형들을 투영된 프리즘의 네 경계 평면으로 클리핑한다. 각 정점마다 적당한 텍스처 좌표를 계산하고 클리핑한 삼각형들을 데칼 텍스처에 매핑한다. 보통 시차 매핑을 사용해 깊이감을 주고, z bias를 조정해 z fighting이 일어나지 않도록 한다.

10.4.3 환경 효과

환경 효과 전용 렌더링 시스템으로 구현한다.

 

10.4.3.1 하늘

단순한 방법

ㄴ 3차원 기하 형상을 렌더링하기 전에 프레임 버퍼에 하늘 텍스처를 채워 넣는다.

ㄴ 하늘 텍스처는 텍셀 대 픽셀 비율이 1:1에 가깝게 렌더링해야 하기 때문에 보통 화면의 해상도와 같거나 거의 비슷하게 만든다.

 

일반적인 방법

ㄴ 픽셀 셰이더의 비용이 비싸기 때문에 하늘 렌더링은 대개 나머지 장면을 모두 그리고 렌더링한다.

 

10.3.4.2 지형

마야 같은 도구를 써서 직접 지형을 모델링하는 방법

높이 필드 지형(높이 필드 텍스처)를 샘플링하여 지형을 모델링하는 방법

지형 제작 툴에는 높이 필드를 색칠할 수 있는 전용 도구를 두는 것이 일반적이다.

ㄴ 여러 개의 텍스처를 블렌딩하여 매핑하는 기능

ㄴ 지형의 일부분에 특수한 지형지물을 일반적인 메시로 만들어 집어넣을 수 있는 기능

 

10.4.3.3 물

물 시뮬레이션 기법은 계속 발전하고 있다.

물의 종류에 따라 특수한 렌더링 기법이 필요한 경우가 보통이다.

물 효과를 구현하는 데는 특수한 물 셰이더와 스크롤 텍스처뿐 아니라 밑 부분의 안개에 쓰이는 파티클 효과, 거품을 표현하는 데칼 형태의 오버레이 등 수많은 요소를 쓸 수 있다.

 

10.4.4 오버레이

헤드업 디스플레이와, GUI 및 메뉴 시스템과 같은 오버레이들은 보통 2차원이나 3차원 그래픽을 뷰 공간이나 스크린 공간에 직접 렌더링하는 형태로 구현한다.

 

10.4.4.1 정규화된 스크린 좌표

2차원 오버레이의 좌표 단위로 스크린 픽셀을 쓰는 것 보다는 다양한 화면 해상도에 적용할 수 있는 정규화된 스크린 좌표(Normalized Screen Coordinates)를 사용하는 편이 낫다.

 

y축은 0.0부터 1.0

x축은 4:3 화면 비율의 경우 0.0부터 4/3, 16:9에서는 0.0부터 16/9의 범위를 갖도록 하는 것이 좋다.

 

10.4.4.3 텍스트와 폰트

텍스트 렌더링 시스템의 핵심은 화면 내의 다양한 위치와 방향에 맞게 텍스트 문자열의 문자 글리프를 순서대로 그리는 기능이다.

여러가지 언어에 따른 문자 세트의 차이와 읽기 방향 등을 처리할 수 있어야 한다.

텍스트에 애니메이션이나 2차원 효과를 지원하는 게임 엔진도 있다.

 

10.4.6 풀 스크린 후처리 효과

스크린 전체의 픽셀을 픽셀 셰이더에 통과시켜 원하는 효과를 적용하게 만든다.

 

모션 블러

ㄴ 컨볼루션 커널을 이미지에 적용해 렌더링된 이미지를 선택적으로 흐리게 만드는 방법으로 구현한다.

 

DoF(Depth of Field)

ㄴ 블러 깊이 버퍼의 내용을 가지고 각 픽셀을 얼마나 흐리게 할 지 조정하는 기법이다.

 

비그넷 (Vignette) 

ㄴ 영화에 주로 사용되는 기법으로, 화면의 모퉁이 부분에서 이미지의 명도, 혹은 채도를 감소시켜 극적인 효과를 낸다.

 

채색 효과 : 스크린 픽셀의 색을 마음대로 변경

ㄴ 예시로 붉은 색을 제외한 다른 모든 색을 회색으로 만드는 표현을 사용할 때

 

9.1 기록과 추적

유용한 디버깅 도구 중 하나는 printf 디버깅이다.

 

9.1.1 Win32 함수 OutputDebugString()을 통한 서식화된 출력

printf와 달리 outputDebugString은 서식화된 출력을 지원하지는 않는다.(일반 문자열만 출력할 수 있다.)

보통의 윈도우 기반 엔진에서는 서식화된 문자열을 따로 만들고 그 문자열을 outputDebugString으로 출력하는 함수를 따로 구현한다.

 

9.1.2 출력 수준 (Vervosity)

코드의 주요한 위치에 printf 구문들을 힘들게 집어넣었는데 볼일이 끝났다고 그냥 지우기에 아까운 경우

출력 수준을 커맨드라인 입력으로 받거나 런타임에 동적으로 지정하도록 할 수 있다.

출력 수준이 최소(보통 0)으로 되어 있으면 치명적 오류 메세지만 출력되고, 출력 수준을 높일수록 더 많은 printf 구문들이 출력을 하도록 만들 수 있다.

쉽게 구현하는 방법은 전역 변수에 출력 수준을 저장하고 출력 함수의 인자로 출력 수준을 받아 조건에 따라 출력을 처리하도록 하는 것이다.

 

9.1.3 채널

디버그 출력을 채널로 분류할 수 있다면 애니메이션 시스템으로 부터 오는 메세지는 애니메이션 채널에 물리 시스템에서 오는 메세지는 물리 채널에 오도록 만들면 보기에 좋다.

디버그 출력 함수에 채널을 지정할 수 있는 인자를 받아 구현할 수 있다.

 

9.1.4 출력을 파일에도 보내기

로그 파일에 디버그 출력을 담으면 문제가 발생한 후에도 원인을 파악할 수 있다.

 

9.1.5 강제 종료 리포트

게임이 강제 종료(Crash) 되는 경우

대부분의 운영체제에서는 최상단 예외 핸들러를 설치해 대부분의 강제 종료 상황을 캐치할 수 있고 예외 핸들러 함수 안에서 필요한 정보를 출력할 수 있다.

출력하면 좋을 정보들

ㄴ 강제 종료 시의 현재 레벨

ㄴ 강제 종료 시 플레이어 캐릭터의 월드 공간 위치

ㄴ 강제 종료 시 플레이어의 애니메이션과 액션 상태

ㄴ 강제 종료 시 실행하고 있던 게임플레이 스크립트

ㄴ 콜 스택 정보

ㄴ 엔진의 모든 메모리 할당자의 상태

ㄴ 기타 강제 종료의 원인을 파악하는데 필요하다고 판단한 정보들

ㄴ 강제 종료 순간의 스크린 샷

 

9.2 디버그 정보 그리기 기능

9.2.1 디버그 정보 그리기 API가 가져야 하는 기능

ㄴ API는 간단하고 사용하기 쉬워야 한다.

ㄴ 기본 단위(선분, 구, 점(작은 구), 좌표계의 축, 경계 박스, 서식화된 문자열)을 그릴 수 있어야 한다.

ㄴ 기본 단위들을 월드 공간 혹은 스크린 공간에 그릴 수 있어야 한다.

ㄴ 깊이 테스트를 켜거나 끌 수 있어야 한다. (장면의 다른 물체에 가려지거나 가려지지 않거나)

ㄴ 코드의 어느 곳에서나 디버그 정보 API를 호출할 수 있어야 한다. 대부분의 렌더링 엔진들에서는 렌더링할 기하 형상들을 매 프레임의 마지막 부분에 전달하기 때문에 모든 디버깅 요청을 큐(디버그 정보 그리기 큐)에 모아 놓았다가 나중에 처리할 수 있게 해야 한다.

ㄴ 모든 디버그 기본 단위에는 수명이 있어야 한다. 프로그래머가 초 단위로 수명을 지정할 수 있어야 한다.

ㄴ 디버그 정보 그리기를 담당하는 시스템은 디버그 기본 단위의 수가 많은 경우라도 효과적으로 처리할 수 있어야 한다.

 

9.3 인게임 메뉴

소스코드를 고치고 컴파일 링크 과정을 거쳐 게임을 다시 실행하기보다 게임 실행 중에 즉시 바꿔 개발 시간을 줄이기 위해 사용한다.

ㄴ 전역으로 선언된 변수 값을 조정하는 기능

ㄴ 임의의 함수를 호출할 수 있는 기능

 

9.4 인게임 콘솔

인게임 메뉴와 비슷한 역할을 한다. 직접 타이핑해야 한다는 점이 다르다.

전역 엔진의 설정을 조정하거나 명령을 내릴 수 있다.

인게임 콘솔에서 스크립트 언어를 지원하는 경우 콘솔을 통해 스크립트로 할 수 있는 모든 것을 할 수 있다.

예를 들면 마인크래프트 /명령어 

 

9.5 디버그 카메라와 게임 일시 정지

게임을 한 프레임 단위로 실행하는 기능

ㄴ 슬로우 모션은 애니메이션, 파티클 효과, 물리 및 충돌 동작, AI 행동 등을 자세히 살펴보는데 도움이 된다.

게임이 정지한 상태에서 자유롭게 게임 월드를 돌아다니며 장면의 모든 구성을 살펴 볼 수 있는 기능

ㄴ 렌더링 엔진과 카메라 컨트롤이 정상적으로 작동하는 동안 게임의 논리적 클록만 정지함으로써 구현

 

9.6 치트

게임 플레이를 테스트하기 위해 게임 규칙을 깨버릴 수도 있어야 한다.

 = 마인크래프트 크리에이티브 모드

 

9.7 스크린샷과 무비 캡처

ㄴ 화면을 캡처해서 .bmp나 .jpg, .tga 포맷으로 디스크에 저장하는 기능, 비디오 메모리에 있는 프레임 버퍼를 메인 메모리로 복사하는 그래픽 API를 사용한다.

 

스크린샷에 추가 기능을 제공한다면

ㄴ 디버깅 그리기 단위를 포함할지의 여부

ㄴ 헤드업 디스플레이를 포함할지의 여부

ㄴ 고해상도의 스크린샷을 저장할 것인지의 여부

ㄴ 카메라 애니메이션을 사용하여 스크린샷을 여러개 찍을 것인지 여부

 

9.8 게임 내 프로파일링

상당수의 엔진들은 어느 정도의 인게임 프로파일링 툴을 제공한다.

측정하고 싶은 코드(함수 호출)에 프로그래머가 표시를 하고 구분할 수 있는 이름을 부여한다. 프로파일링 툴은 이렇게 표시된 코드들의 실행 시간을 CPU의 정밀 타이머로 측정한 후 결과를 메모리에 저장한다. 이렇게 측정한 결과는 헤드업 디스플레이를 통해 보여준다. 실행 시간을 보여줄 때  cpu cycle 수, 마이크로초 단위, 전체 프레임에서 차지하는 비율로 보여줄 수 있다.

 

9.9 인게임 메모리 상태와 누수 감지

게임 엔진들은 자체적으로 메모리 감지 툴을 구현한다. 이런 툴의 역할은 각 엔진 하부 시스템에서 사용하는 메모리 양을 보여주고 메모리 누수 여부를 감지한다.

 

게임이 실제로 사용하는 메모리 양을 정확히 파악하기 어려운 이유

1. 외부 라이브러리를 사용하는 경우 라이브러리의 할당자를 원하는 할당자로 대체할 수 없는 경우 메모리 할당/해제를 추적할 수 없다.

2. 비디오 메모리의 사용량을 추적하기는 힘들다. DirectX API가 비디오 메모리의 할당과 사용에 대한 세부적 내용을 개발자로부터 격리시키기 때문이다.

3. 할당자의 종류가 많고 할당자들은 게임이 시작할 때 큰 단위로 메모리를 미리 할당하고(new로) 자체적으로 관리하기 때문에 실제로 쓸 만한 정보를 얻으려면 각 할당자의 메모리 블록 안에서 할당과 해제를 추적해야 한다.


게임을 개발하는 도중에 사용하는 PC나 콘솔은 보통 최소 요구 사양 PC나 실제 콘솔보다 RAM도 많고 성능이 좋기 때문에 실제 상황에서는 메모리가 부족한데 잘 실행되는 경우가 있다. 이를 위해 메모리가 부족 상태가 되면 게임 엔진은 메모리가 부족하다는 메세지를 띄워주어야 한다.

 

메모리 추적 시스템을 이용해 리소스 자원을 불러오는데 실패한 경우 대체 리소스를 입혀주거나 불러오지 못한 리소스의 이름을 출력하는 시스템을 만들 수도 있다. 이는 프로그래머가 문제를 빠르게 파악할 수 있게 도움을 준다.

 

HID는 플레이어가 게임에 입력을 보낼 수 있게 해주는 장치(키보드, 조이스틱 등)이다.

8.2 HID와 인터페이스

게임 소프트웨어가 HID로부터의 입력을 읽어오고 출력을 보내는 방법은 여러가지가 있다.

 

8.2.1 폴링

ㄴ 하드웨어를 주기적으로 폴링하는 방식으로 입력을 읽어온다.(하드웨어의 상태를 보통 게임 루프당 한 번씩 읽는다)

ㄴ 하드웨어 레지스터 혹은 메모리 맵 I/O 포트를 직접 읽는다.

 

8.2.2 인터럽트 

ㄴ 컨트롤러의 상태가 변할 때만 게임 엔진에 데이터를 보낸다. 

ㄴ HID가 인터럽트를 하드웨어에 보내 메인 프로그램을 잠시 정지시키고 장치의 상태를 읽도록 만든다.

 

8.2.3 무선 장치

ㄴ 레지스터나 메모리 맵 I/O 포트를 통해 데이터를 읽고 쓸 수 없다.

ㄴ 블루투스 프로토콜을 통해 장치와 교신해야 한다. 게임 엔진의 메인 루프와는 별도로 마련된 스레드에서 처리하는 경우가 일반적이며, 그렇지 않은 경우라면 적어도 메인 루프에서 호출할 수 있게 비교적 단순한 인터페이스를 통해 캡슐화한다.

 

8.3 입력의 종류

8.3.1 디지털 버튼

ㄴ 버튼의 눌린 상태와 텐 상태를 비트 하나 (0과 1)로 표현하는 경우가 많기 때문에, 하나의 정수 타입 변수에 모든 버튼의 상태를 저장할 수 있다. 개별 버튼의 상태를 알고 싶으면 버튼의 비트 마스크 값과 AND 연산한 후 값이 0이 아닌지 보면 된다.

 

8.3.2 아날로그 축과 아날로그 버튼

엑스박스 360의 게임패드를 예시로 들어을 때

조이스틱의 아날로그 스틱(아날로그 축)의 입력을 x축 y축 좌표값으로 각각 16비트 정수에 저장하고

아날로그 버튼(누르는 세기에 따라 값이 다른 버튼)은 8비트 정수에 저장한다.

 

8.3.4 가속도계

듀얼쇼크에는 가속도계(accelerometers)라 불리는 가속 감지 센서가 있는데 x, y, z축 방향의 가속도를 3G까지 측정할 수 있고 각 축당 8비트 정수에 저장한다.

 

8.4 출력의 종류

여기에서는 HID가 플레이어에게 주는 출력을 말한다.

8.4.1 럼블(진동)

ㄴ 하나 이상의 모터를 사용하고 모터의 속더를 사용하여 진동을 낸다.

 

8.4.2 포스 피드백

ㄴ 모터를 통해 조작하려는 방향에 살짝 저항하는 힘을 가하는 것

 

8.4.3 오디오

ㄴ 컨트롤러에 작은 스피커가 달려 있다.

 

8.5 게임 엔진의 HID 시스템

HID와 게임 사이에 최소한 하나 이상의 간접 지정 레벨을 두어 다양한 방식으로 HID 입력을 추상화한다.

8.5.2 데드 존

아날로그 축의 경우 가만히 두면 0의 입력값으로 취급해야 하는데 아날로그 장비이므로 잡음에 의해 완벽한 0이 아닐 수 있다. 데드 존을 두어 데드 존 안에 포함되는 입력을 모두 0으로 처리하는 방법을 사용한다. 데드 존의 크기는 잡음을 처리할 수있을 정도로는 넓어야 하지만 조작감이 둔하다고 느낄 정도로 커서는 안 된다.

 

8.5.3 아날로그 신호 필터링

데드존 밖으로 오는 잡음을 처리하기 위해 원본 입력을 게임에 사용하기 전에 저역 통과 필터를 거치게 하는 방법을 사용한다.

지난 프레임의 필터에 거친 입력 값과 현재의 원본 입력 값을 결합하는 방법

지난 3프레임 동안의 입력 데이터의 이동 평균을 사용하는 방법

 

8.5.4 입력 이벤트 감지

8.5.4.1 버튼 업과 버튼 다운

지난 프레임에서 저장한 버튼 상태 값과 현재 프레임에서 관찰한 상태 값을 비교하여 값이 다른 경우 이벤트가 발생했다는 것을 알 수 있다. 

이전 프레임에서의 버튼 상태를 저장하고 있어야 한다.

 

8.5.4.2 코드(chord)

동시에 눌러서 게임에 특수한 행동을 하도록 유도하는 버튼 그룹을 코드라고 한다.

 

ㄴ 개별 버튼의 다운을 감지했을 때 코드의 다른 버튼이 동시에 눌리지 않았는지 검사하는 방식으로 구현한다.

ㄴ 사람은 기계가 아니기 때문에 코드의 버튼을 누를 때 다소의 시간차가 있을 수 있다는 점도 고려해야 한다. i 프레임에서 코드의 일부를 감지하고, 다음 i + N 프레임에서 나머지 코드를 감지하는 경우도 처리할 수 있어야 한다.

 

8.5.4.3 시퀀스와 제스처 감지

격투 게임이나 브롤러 게임에서는 일련의 버튼 순서를 감지하는 경우가 있다.

 

짧은 시간 안에 정해진 순서의 버튼을 입력해야만 올바른 동작이 되는 제스처를 감지하기 위해서는 짧은 시간동안 플레이어가 수행한 HID 조작을 기록하는 방법을 사용한다.

제스처를 이루는 첫 입력이 들어올 때 기록 버퍼에 입력 값과 시각을 기록하고, 그 후 이어지는 입력이 감지되는 경우 직전 입력의 시각과 현재 입력의 시각을 비교한다. 그 차이가 허용된 시간 범위 내인 경우 현재 입력도 기록 버퍼에 저장한다.

 

제한 시간 내에 전체 시퀀스의 입력이 모두 완료된 경우 이벤트를 발생해 게임 엔진에 제스처가 일어났음을 알린다. 중간에 올바르지 않은 입력이 들어오거나 제스처 코드가 제한 시간을 넘어서 들어온 경우에는 기록 버퍼를 전부 비운다.

 

8.5.7 입력 재배치

간접 지정 레벨을 둠으로써 사용자가 모든 키를 재지정할 수 있도록 한다.

 

8.5.8 상황 기반 컨트롤

ㄴ 게임 내에서 하나의 키로 많은 처리를 할 경우 (상호작용 버튼)에는 플레이어가 어떤 상태에 있느냐에 따라 다른 처리를 할 수 있도록 해야 한다. 어떤 상태에 있는지를 정하는 것이 까다롭다.

 

8.5.9 입력 무시

ㄴ 플레이어가 인게임 시네마틱을 보고 있는 경우에는 캐릭터를 조작할 수 없게 해야한다.

 

7.1 렌더링 루프

while(!quit) {
    // 플레이어 입력을 받아 카메라를 업데이트 
    
    // 프레임 자원 인덱스 변경 
    
    // 장면에 존재하는 동적인 요소들의 위치, 회전, 크기 행렬 변경
    
    // 후면 버퍼에 그리기
    
    // 후면 버퍼의 내용을 전면 버퍼에 복사(swap)
}

 

7.2 게임 루프

게임은 상호작용하는 다양한 하부 시스템으로 이루어지는데, 장치 I/O, 렌더링, 애니메이션, 충돌 감지 및 처리, 부가적인 강체 역학 시뮬레이션, 멀티플레이어 네트워크, 오디오 등 다양한 부분들이 게임을 구성한다.

대부분의 하부 시스템은 게임이 돌아가는 동안 주기적으로 업데이트 해야 하지만 하부 시스템마다 주기가 다르다.

 

가장 단순한 방법은 루프 하나에 하부 시스템의 루프를 모두 넣는 방법이다.

 

7.3 게임 루프 구조의 형태

7.3.1 윈도우 메세지 펌프

DirectX3D 루나 책에서 사용하는 예제

윈도우 플랫폼에서 돌아가는 게임들은 엔진 하부 시스템들뿐 아니라 운영체계에서 오는 여러 메세지도 처리해야 하기 때문에 메세지 펌프라는 코드가 존재한다. 윈도우 메세지가 없을때만 게임의 엔진에 대한 처리를 한다.

게임 윈도우의 크기를 바꾸거나 바탕화면에서 창을 마우스로 끌고 있으면 게임은 멈춘다.

 

7.3.2 콜백 주도 프레임워크

게임 엔진 하부 시스템과 외부 게임 미들웨어 패키지들이 라이브러리 형태로 되어 있는 경우

ㄴ 프로그래머의 선택 폭이 넓어지지만 라이브러리의 함수와 클래스를 어떻게 사용할지 프로그래머가 잘 이해하고 있어야 한다.

게임 엔진 하부 시스템과 외부 게임 미들웨어 패키지들이 프레임워크 구조로 되어 있는 경우

ㄴ 게임 루프가 이미 짜여져 있지만 비어 있다. 비어 있는 세부적인 부분들을 채워 넣기 위해 게임 프로그래머는 콜백 함수를 짠다.

 

7.3.3 이벤트 기반 업데이트

이벤트 시스템을 활용해 주기적으로 하부 시스템을 업데이트하도록 구현하는 방법도 있다.

ㄴ 현재 시점보다 미래에 이벤트를 보낼 수 있는 기능을 이벤트 시스템이 지원해야 한다.(이벤트 큐에 저장했다가 나중에 보낼 수 있어야 한다.)

 

7.4 Abstract Timelines

7.4.1 실시간

CPU의 정밀 타임 레지스터 값으로 측정하는 시간을 사용할 수 있다.

예를 들어 3GHZ 펜티엄 프로세서는 1초에 30억번 타이머의 값을 증가시킨다.

이 경우 정밀도는 1/30억 초 = 0.333ns(1/3 나노초)가 된다.

마이크로프로세서마다, 운영체제마다 정밀 타이머의 값을 얻어오는 방법은 다르다. Win32 API에서는

QueryPerformanceCounter() 함수를 사용하여 64비트 카운터 레지스터를 읽어오고

QueryPerformanceFrequency() 함수를 사용하여 현재 CPU에서 초당 카운터를 몇 번 증가시키는지를 받는다.

 

7.4.2 게임 시간

ㄴ 타임라인에 실시간만 있는 것은 아니다. 필요한 만큼 다른 타임라인을 정의할 수 있다.

ㄴ 여기에서 말하는 게임 시간은 FPS 게임에서의 라운드당 시간 같은 것이다.

ㄴ 게임을 일시 정지하고 싶은 경우에는 게임 시간을 업데이트 하지 않으면 되고 게임 시간을 슬로우 모션으로 보이고 싶은 경우 게임 시간을 천천히 업데이트 하면 된다.

ㄴ 게임 시간을 정지하거나 느리게 하는 것은 디버깅할 때 도움이 된다.

 

7.4.3 로컬 타임라인과 글로벌 타임라인

애니메이션 클립이나 오디오 클립의 타임라인은 로컬 타임라인이라고 할 수 있다.

ㄴ 로컬 타임라인의 시작점을 글로벌 타임라인의 원하는 위치에 매핑하면 클립을 재생할 수 있다.

ㄴ 로컬 타임라인의 크기를 줄이거나 뒤집으면 재생 속도를 조절하거나 거꾸로 재생하는 기능을 구현할 수도 있다.

 

7.5 시간을 측정하는 방법과 처리하는 방법

7.5.1 프레임 레이트와 시간 델타

두 프레임 사이에 시간이 얼마나 흘렸는지를 나타내는 말에 [프레임 시간, 시간 델타, 델타 시간] 등의 용어를 사용한다.

게임을 30FPS로 렌더링하면 델타 시간은 1/30초 (=16.6 ms, 16.6 밀리초)

밀리초라는 단위를 많이 사용한다.

 

7.5.2 프레임 레이트와 속도의 관계

게임에서 움직이는 물체의 위치 변화를 표현하기 위해서는 프레임마다 변위값을 더해주는 방법을 사용하는데, 변위값이 고정되어 있다면 델타 시간에 따라 속도가 다르게 보일 것이다.

하지만 델타 시간은 CPU 속도에 따라 달라질 수 있으므로 CPU에 따라 게임이 다르게 동작할 여지가 있다.

 

7.5.2.2 경과 시간에 따른 업데이트 

움직이는 물체의 위치 변화를 표현할 때 델타 시간을 사용하여 변위 값을 계산하는 방법이다

이 방식은 k 프레임의 시간을 측정하여 k + 1 프레임의 시간에 대한 예측 값으로 사용하기 때문에 정확하다는 보장이 없다.

ㄴ 레이트 스파이크 현상이라고 한다.

 

7.5.2.3 이동 평균 사용

ㄴ 순간적인 성능 스파이크로 인한 부작용은 줄어들지만 프레임 레이트 변화에 즉각 대응하기에는 어렵다.

 

7.5.2.4 프레임 레이트 조절

ㄴ 다음 프레임의 시간을 추측하는 대신 모든 프레임의 시간을 고정시킨다.

ㄴ 현재 프레임 시간을 측정하여 목표 시간 (30FPS 기준 33.ms)보다 짧은 경우, 목표 시간이 채워질 때까지 메인 스레드를 잠들게 한다. 목표 시간보다 긴 경우, 한 프레임을 더 기다린다.

ㄴ 일부 엔진 시스템, 대표적으로 물리 시뮬레이션에 쓰이는 수치 적분 모듈 같은 경우 일정한 간격으로 업데이트 했을 때 최적의 성능을 낸다.

ㄴ 테어링을 방지하는데도 도움이 된다.

ㄴ 게임 내 녹화 및 재생 기능의 안정성이 높아진다. (게임 플레이를 녹화한 후 그대로 재생하는 기능)

 

7.5.2.5 화면 간격과 V-sync

메인 게임 루프의 프레임 레이트가 스크린의 재생 빈도의 배수가 되게 조정

 

7.5.3 정밀 타이머로 실제 시간 측정

= 7.4.1 내용

 

7.5.4 시간 단위와 클록 변수

어떤 시간 단위를 사용할 것인가? [초, 밀리초, 하드웨어 주기]

측정한 시간 값을 어떤 데이터 타입에 저장할 것인가? [64비트 정수, 32비트 정수, 32비트 부동소수]

 

7.5.4.1 64비트 정수 클록

하드웨어 주기 단위(CPU 주기 단위)로 측정하는 경우에 가장 정확도가 높은 방법이다.

값의 범위가 크다 (약 195년)

 

7.5.4.2 32비트 정수 클록

하드웨어 주기 단위로 사용하되 짧은 기간을 측정하는 경우 사용할 수 있다.

64비트 정수값(이후 측정 시간) - 64비트 정수값(이전 측정 시간) 의 결과를 32비트 정수에 저장한다.

 

7.5.4.3 32비트 부동소수 클록

CPU 주기 단위로 측정된 값을 CPU 클록 주파수 (3GHZ 펜티엄 프로세서의 경우 30억)로 나눈 값을 초 단위의 부동소수로 저장하는 방식이다.

짧은 기간에 대해서만 사용하는 것이 좋다.

 

7.5.4.4 부동소수 클록의 한계

ㄴ 기간이 길어져 클록의 절대값(CPU 주기 단위로 측정된 값)이 커지면 정수 부분에 많은 비트가 쓰여 소수 부분에 쓸 비트가 줄어들게 되어 소수부분의 정확도가 떨어질 수 있다.

 

7.5.4.5 기타 시간 단위

1/300초를 자주 사용한다.

 

7.5.5 중단점과 시간

게임을 실행하다가 중단점에 도달하면 게임 루프는 멈추고 디버거가 실행된다. 중단된 프로그램을 재개하는 순간 엄청나게 큰 델타 시간이 엔진 하부 시스템에 전달되면 이상한 문제가 생길 수 있다.

델타 시간이 미리 정한 한계(예, 1/10초)를 벗어나면 목표 프레임 레이트로 강제 설정하는 방법으로 해결 가능하다.

 

7.6 멀티프로세서 게임 루프

멀티 프로세서 시대가 도래하면서 한 개의 게임 루프가 모든 하부 시스템을 담당하지 않게 되었다.

 

7.6.2 SIMD (single instruction multiple data)

오늘날의 거의 모든 CPU가 SIMD 명령어들을 지원한다.

CPU에 따라 지원하는 명령어는 조금씩 다르지만 게임에서 가장 많이 쓰는 것은 32비트 부동소수점 수 4개를 병렬로 계산하는 명령어들이다.

 

7.6.3 분기와 결합

멀티코어를 활용하는 방안으로 분할 정복 알고리즘을 병렬화에 적용하는 방안이 있다.

문제를 작은 단위로 쪼갠 후 이것들을 여러 개의 코어, 혹은 하드웨어 스레드에 분배하고 (분기, fork) 모든 작업이 끝나면 그 결과를 합치는 (결합, join) 것이다.

 

예를 들어 선형 보간을 통해 애니메이션을 블렌딩할 때 뼈대의 각 관절들은 다른 관절들과 독립적으로 보간되기 때문에 병렬적으로 처리할 수 있다. 처리해야 할 관절이 500개라면 각 스레드나 코어에 500개를 분배

 

7.6.4 하부 시스템마다 스레드 하나씩 두기

특정 엔진 하부 시스템을 별도의 스레드로 돌게 하는 방식이다.

이런 디자인 방식은 어느 정도 분리된 역할을 반복적으로 맡아 하는 엔진 하부 시스템에 적합하다. (물리, 렌더링, 애니메이션 파이프라인, 오디오 엔진)

메인 스레드는 하이레벨 로직(메인 게임 루프)에 대한 처리를 담당하면서도 하부 시스템 스레드를 제어하고 동기화하는 역할을 동시에 한다.

스레드 아키텍처를 사용하려면 일반적으로 하드웨어 플랫폼의 스레드 라이브러리를 이용한다. 윈도우는 Win32 스레드 API를 사용한다.

 

7.6.5 잡 (스레드와 비슷하지만 작은 단위)

하부 시스템을 멀티스레드로 분리하는 접근 방식의 문제점 중 하나는 각 스레드가 비교적 큰 단위의 작업을 처리하기 때문에 한 스레드가 일을 제때 끝마치지 못하면 메인 게임 루프를 포함한 다른 스레드의 진행도 멈출 수 있다는 문제가 있다는 것이다. (게임 루프의 유연성이 떨어진다)

 

잡 구조에서는 작업을 작은 단위의 덩어리로 쪼개서 여유가 있는 어떠한 프로세서에서도 실행할 수 있게 만든다.

 

7.6.6 비동기 프로그램 디자인

비동기 디자인의 경우 요청 함수를 호출하면 요청을 잡 큐에 넣은 후 즉시 리턴한다.

보통 작업 요청을 한 후 다음 프레임에서 결과를 받아 처리하는 경우가 많다.

 

7.7 네트워크 멀티플레이어 게임 루프

7.7.1 클라이언트-서버

게임 로직의 거의 모든 부분이 하나의 서버에서 돌아간다.

클라이언트는 단순한 렌더링 엔진으로써 로컬에 있는 플레이어 캐릭터를 조정하는 역할만 하고, 그 외에는 서버가 그리라고 지시하는 것들을 화면에 보여주기만 한다.

클라이언트 코드를 짤 때에는 로컬 플레이어의 입력이 화면 속 플레이어 캐릭터의 움직임에 즉시 반영되도록 해야한다.

 

서버를 별도의 전용 머신에서 돌리는 경우에는 전용 서버 모드(dedicated server)라고 하고

클라이언트 머신 중 하나가 서버를 같이 돌리고 있는 경우는 서버 위 클라이언트 모드(client-on-top-of-server)라고 한다.

 

서버와 클라이언트의 게임 루프가 반드시 같은 빈도로 업데이트되야 하는 것은 아니다.

 

1. 클라이언트와 서버를 별개의 프로세스로 구현하는 방법

 

2. 한 프로세스 안에서 두 개의 스레드로 구현하는 방법

 

1, 2번 방법은 서버 위 클라이언트 모드로 동작하는 경우, 로컬에서 서로 통신하는데 상당한 오버헤드가 든다.

 

3. 한 개의 스레드 안에서 클라이언트와 서버를 같이 구동하고 하나의 게임 루프로 관리하는 방식을 사용할 수도 있다.

ㄴ 같은 게임 루프에서 서버는 20FPS로 업데이트하고 클라이언트는 60FPS로 업데이트 하는 방식

 

7.7.2 피어 투 피어 (p2p)

모든 머신이 어느 정도 서버 역할을 맡는다.

각 머신은 게임의 동적 객체들을 일정 부분 담당하는데, 한 객체에 대해서는 오직 한 머신만 독점적으로 관리한다.

로컬 머신이 직접 관리하는 게임 객체(진짜 객체)와 다른 머신이 관리하는 게임 객체(가짜 객체)를 구분할 수 있어야 한다.

로컬 머신이 관리하는 객체에는 완전한 기능을 갖춘 진짜 게임 객체를 사용하고, 그렇지 않은 객체에 대해서는 최소한의 상태만 갖는 프록시 버전을 사용한다.

객체에 대한 관리 권한이 머신 사이에 동적으로 변할 수 있기 때문에 복잡하다. (게임 객체를 관리하던 머신의 연결이 끊기면 그 머신이 관리하던 게임 객체는 다른 머신들이 가져가 관리해야 한다.)

 

리소스 시스템은 파일 시스템을 많이 사용한다. 게임엔진에서는 운영체제의 파일 시스템 API를 그대로 사용하지 않고 이를 감싼 랩핑 API를 엔진에 구현하여 사용하는 경우가 많다. 

이렇게 구현하는 이유

1. 멀티 플랫폼 지원을 위해

ㄴ 엔진의 파일 시스템 API를 사용하게 되면 엔진의 나머지 부분에서는 플랫폼의 차이를 신경쓰지 않아도 된다.

2. 운영체제의 파일 시스템 API가 게임 엔진에서 필요로 하는 모든 기능을 지원하지 못하는 경우가 있기 때문에

ㄴ 파일 스트리밍을 지원하는 엔진은 많지만 운영체제는 많지 않다.

 

6.1 파일 시스템

파일 이름과 경로를 다루는 기능

파일들을 열고 닫는 기능과 읽고 쓰는 기능

디렉토리의 내용을 검색하는 기능

스트리밍을 위한 비동기 파일 I/O 요청을 처리하는 기능

 

6.1.1 파일 이름과 경로

6.1.1.1 운영체제에 따른 차이

윈도우

ㄴ 다중 볼륨 지원 D: C: (볼륨을 나타내는 첫 번째 방법)

ㄴ \\ [원격 컴퓨터 이름] (볼륨을 나타내는 두 번째 방법)

ㄴ 파일 이름에 콜론을 사용할 수 없다.

ㄴ 경로 구분 문자로 \ 를 사용

유닉스

ㄴ 볼륨마다 개별적인 디렉터리 구조 를 가질 수 없다. 하나의 통합된 구조이다.

ㄴ 경로 구분 문자로 / 를 사용

mac os

ㄴ 콜론을 경로 구분 문자로 사용한다.

 

6.1.1.3 검색 경로

검색 경로는 경로들의 리스트를 담고 있는 문자열로 콜론이나 세미콜론 등의 특수한 문자로 구분된다.

리소스 파일의 검색 경로를 담은 텍스트 파일을 사용하여 런타임에 자원의 경로를 얻는 방법도 있지만 일반적인 경우 엔진은 자원의 경로를 미리 알고 있다.

 

6.1.1.4 경로 API

ㄴ 경로를 처리할 때 파일 이름 및 확장자를 분리하는 일

ㄴ 절대 경로와 상대 경로를 전환하는 일

윈도우 API shl-wapi.dll에 구현되어 있다.

 

6.1.2 기본 파일 I/O

파일 열기, 읽기, 쓰기에 두 가지 종류의 API가 있다.

1. 버퍼를 사용하는 방식

ㄴ API가 알아서 입력 및 출력 데이터 버퍼를 관리

2. 버퍼를 사용하지 않는 방식

ㄴ 프로그래머가 데이터 버퍼를 할당하고 관리

 

표준 C 라이브러리 함수

ㄴ fopen, fread, fclose, fwrite (버퍼를 사용하는 방식)

 

운영체제가 제공하는 함수

open, read, close, write (버퍼를 프로그래머가 관리하는 방식, 시스템 콜)

 

6.1.2.1 운영체제의 함수를 감싸는 경우와 그렇지 않은 경우

게임 엔진의 I/O API를 구현할 때 두 가지 방식으로 구현 할 수 있다.

 

1. 운영체제의 I/O API를 감싸 엔진의 I/O API를 구현하는 방식

2. 표준 C 라이브러리의 I/O 함수를 사용하는 방식

 

운영체제의 I/O API를 감싸 엔진의 I/O API를 구현하는 방식의 장점

ㄴ 서로 다른 플랫폼에서 똑같은 동작이 보이게 보장할 수 있다.

ㄴ 엔진에서 실제 필요한 기능들만 갖게 API를 단순화할 수 있다.

ㄴ 확장 기능을 지원할 수 있다. (하드 디스크, 네트워크, 휴대형 미디어에 담긴 파일의 처리)

 

6.1.2.2 동기적 파일 I/O

ㄴ 표준 C 라이브러리의 파일 I/O 라이브러리는 동기적으로 동작한다. (fread() 구문에서 block이 걸린다.)

 

6.1.3 비동기적 파일 I/O

ㄴ 스트리밍을 지원하기 위해서는 비동기 파일 I/O 라이브러리를 활용해야 한다.

ㄴ 스트리밍은 오디오, 텍스처 데이터 등 모든 데이터를 대상으로 사용 가능하다.

ㄴ 비동기적 I/O 라이브러리는 메인 프로그램에서 요청한 후 I/O 동작이 완료되기를 기다리는 기능도 지원하는 경우가 일반적이다. (asyncWait 함수)

ㄴ 비동기 연산 시간이 얼마나 걸릴지 예상 시간을 알 수 있는 기능을 제공하기도 한다.

 

6.1.3.1 우선순위 

비동기 I/O 연산에는 보통 우선순위가 있다. 예를 들어 오디오를 하드디스크에서 스트리밍하는 작업은 보통 게임 레벨의 개체들을 불러오는 일보다는 우선순위가 높아야 한다.

 

I/O 시스템은 우선순위가 높은 요청이 제한 시간 안에 완료될 수 있게 우선순위가 낮은 요청을 잠시 정지시킬 수 있어야 한다.

 

6.1.3.2 비동기 파일 I/O의 동작 방법

I/O 요청을 별도의 스레드에서 처리하는 방식으로 동작한다.

1. 메인 스레드에서 비동기 I/O 함수를 호출하면 큐에 요청을 넣는다.

2. I/O 스레드에서 큐에서 요청을 뽑아 동기적 I/O 함수를 사용하여 처리한다.

3-1. 작업이 완료되면 메인 스레드가 지정한 콜백 함수를 호출해 요청이 완료되었음을 알린다.

3-2. I/O 요청을 할 때 콜백 함수를 지정하지 않는 대신 세마포어를 이용하면 메인 스레드에서 I/O 작업이 완료될 때까지 기다린다.

 

6.2 리소스 매니저

리소스의 종류

ㄴ 메시, 머터리얼, 텍스처, 셰이더 프로그램, 애니메이션, 오디오 클립, 레벨 레이아웃, 충돌 기본 단위, 물리 매개변수 등

 

리소스 매니저가 해야 할 일 2가지

1. 리소스를 만들어 내는 오프라인 툴 체인을 관리

2. 런타임에 자원을 올리고 내리는 일을 관리

 

6.2.1 오프라인 리소스 관리와 툴 체인

6.2.1.1 자원의 리비전 컨트롤

자원 파일들을 상용 버전 관리 프로그램을 사용하거나 이런 시스템을 감싼 툴을 제작해 사용하기도 한다.

 

발생하는 문제

ㄴ 아트 파일들은 용량이 커서 중앙 저장소에서 로컬로 파일을 복사하는 방식은 효율이 떨어진다.

ㄴ 심볼릭 링크를 활용해 데이터를 복사하지 않고 저장소의 자원을 로컬 머신에서 볼 수 있는 툴을 만들면 해결 가능하다.

 

6.2.1.2 리소스 데이터베이스

자원의 메타 데이터(자원을 어떤 방식으로 가공할 것인지)를 저장하는 데이터베이스가 필요하다.

메타 데이터는 리소스 자원 안에 포함되어 있는 경우도 있다.

 

리소스 데이터베이스가 제공해야 할 기능

ㄴ 여러 종류의 리소스를 처리할 수 있는 기능

ㄴ 새 리소스를 만들 수 있는 기능

ㄴ 리소스를 지울 수 있는 기능

ㄴ 기존의 리소스를 살펴보고 수정할 수 있는 기능

ㄴ 리소스의 원본 파일을 디스크의 다른 장소로 옮길 수 있는 기능

ㄴ 리소스들끼리 교차 참조할 수 있는 기능, 교차 참조에 대한 참조 무결성을 유지하는 기능

ㄴ 리비전 히스토리를 관리하는 기능 (작업자, 작업내용에 대한 로그)

 

6.2.1.4 자원 다듬기 파이프라인 (ACP : asset conditioning pipeline)

 

1. 내보내기 도구 (Exporter)

ㄴ 보통 DCC(digital cotent creation) 프로그램이 기능을 지원한다.

 

2. 리소스 컴파일러 (컴파일 과정 없이 export 단계 이후 바로 사용할 수 있는 자원도 존재한다.)

ㄴ 데이터를 게임에 적합한 형태로 바꾸거나 계산이 필요하다면 데이터를 이용해 계산을 하는 일들을 진행한다.

 

3. 리소스 링커 (export와 컴파일 단계만 거친 후 바로 게임에서 사용할 수 있는 자원들은 필요 없는 단계)

ㄴ C++ 프로그램을 컴파일하는 과정에서 여러 obj 파일들을 하나의 실행 파일로 링크하는 것과 비슷하게 3D 모델과 같은 복합 리소스를 빌드하는 경우, 메시 파일, 머터리얼 파일, 뼈대 파일, 애니메이션 파일들을 하나의 리소스 파일로 묶어야 하기 때문에 필요한 단계이다.

 

리소스 의존 관계의 빌드 규칙

소스 파일에도 상호 의존성이 있듯 게임 자원에도 의존성이 있다.

메시가 참조하는 머터리얼이 여러 개 있을 수 있고, 머터리얼들은 여러 텍스처를 참조할 수 있다.

파이프라인에서 자원을 처리하는 순서는 상호 의존성에 영향을 받는다.

자원들을 올바른 순서로 빌드할 빌드 툴이 있어야 한다.

 

6.2.2 런타임 리소스 매니저

6.2.2.1 런타임 리소스 매니저의 역할

ㄴ 각 고유한 리소스는 메모리에 하나만 존재하도록 한다.

ㄴ 각 리소스의 수명을 관리한다.

ㄴ 필요한 리소스를 불러오고 더 이상 필요 없는 리소스는 내린다.

ㄴ 복합 리소스의 로딩을 처리한다. 3D 모델 같은 거

ㄴ 참조 무결성을 유지한다. (복합 리소스를 불러올 때 상호 참조 관계로 연결된 리소스들을 모두 불러올 수 있어야 한다.)

ㄴ 불러온 리소스들의 메모리 사용량을 관리하고 메모리의 적절한 곳에 저장한다.

 

6.2.2.2 리소스 파일과 디렉터리 구조

ㄴ pc 엔진은 보통 리소스 파일들을 디스크에 두고 디렉터리 트리로 관리한다.

 

여러 개의 리소스 파일들을 하나의 ZIP 파일이나 복합 파일로 압축하여 관리하는 엔진도 있다.

ㄴ 로딩 시간이 줄어들 수 있다. 하드 디스크의 연속적인 공간에 모든 데이터가 저장되기 때문에 탐색 시간(헤더 옮기는 시간)이 줄어들고, 파일을 여는 데 드는 시간도 여러 번에서 한 번으로 줄기 때문에

 

ZIP 형식을 사용할 경우 얻는 이점

1. ZIP 파일을 읽고 쓰는데 사용하는 무료 라이브러리가 존재한다. (zlib, zziplib)

2. ZIP 파일 안의 가상 파일들은 일반 파일 시스템과 마찬가지로 상대 경로로 접근할 수 있다.

3. ZIP 파일은 압축이 가능하기 때문에 디스크에서 리소스가 차지하는 용량을 줄일 수 있다.

4. ZIP 파일을 모듈로써 사용할 수 있다. (현지화해야 할 자원들을 지역 별로 묶어 지역별로 다른 ZIP 파일을 둘 수 있다.)

 

언리얼의 경우 패키지 (.pak) 라고 불리는 자체 제작한 복합 파일을 사용한다. 

 

6.2.2.3 리소스 파일 형식

자체 제작한 리소스 파일 형식을 사용하는 이유

ㄴ 표준화된 형식으로 엔진에서 쓰이는 모든 정보들을 표현하지 못하는 경우가 있기 때문에

 

가능한 한 많은 오프라인 처리를 통해 런타임에 처리할 작업을 줄이는 방법의 예

ㄴ 특정한 형태로 리소스 데이터를 메모리에 배열해야 하는 경우 오프라인 툴에서 데이터를 배열하게 하고

메모리에 올리기 위해 이진 데이터 형식을 사용할 수도 있다. (런타임에 리소스 데이터를 로드한 후 배열하데 사용되는 시간을 줄일 수 있다.)

 

6.2.2.4 리소스 GUID(Globally unique identifier)

GUID로 흔히 쓰이는 것은 리소스의 파일 시스템 경로(문자열)이다.

 

언리얼 엔진같은 경우는 패키지라는 큰 파일 안에 여러 리소스들을 저장하기 때문에 패키지 파일의 경로가 각 리소스를 고유하게 나타내지는 못하기 때문에 GUID를 파일 시스템 경로로 사용하지 못한다.(대신 해시 코드를 사용한다)

이를 해결하기 위해 패키지 안의 각 리소스는 파일 시스템 경로와 유사한 고유한 이름을 갖는다. 언리얼 엔진에서의 리소스 GUID는 패키지 파일의 이름 + 패키지 안의 리소스에 대한 경로를 합쳐 만들어진다.

 

6.2.2.5 리소스 레지스트리

리소스 매니저는 레지스트리를 이용하여 하나의 리소스만 메모리에 존재하도록 보장한다.

키-값 자료구조를 갖는 map, hashmap을 이용해서 GUID를 키로 사용하고 값은 리소스의 메모리 주소에 대한 포인터를 사용한다.

 

게임에서 리소스를 요청하는 경우 리소스 매니저는 리소스 레지스트리를 검색하여 리소스가 존재하면 리소스의 포인터를 반환하고 없다면 메모리에 리소스를 올리거나 그냥 실패 코드를 리턴하게 할 수 있다.

 

리소스 로드 작업은 시간이 많이 걸리는 작업(디스크에서 파일을 읽어 큰 용량의 리소스를 올려야 함) 이기 때문에 두 가지 방법으로 보통 처리한다.

 

1. 게임 플레이 중에는 아예 리소스를 로드하지 않는다.

ㄴ 게임 플레이가 시작되기 전에 미리 로드하는 방법

 

2. 리소스 로드를 비동기적으로 수행한다.

ㄴ 게임 플레이 도중 다른 레벨의 리소스를 미리 불러온다면 로딩 화면을 보지 않아도 된다.

 

6.2.2.6 리소스 수명

리소스의 수명은 메모리에 올라가는 순간부터 메모리에서 내려오는 순간까지를 뜻한다.

 

게임을 처음 시작할 때 불러 와서 게임이 진행되는 동안 항상 메모리에 상주해야 하는 리소스들의 수명은 사실상 무한대이기 때문에 글로벌 리소스 또는 글로벌 에셋 리소스라고 부른다.

 

글로벌 에셋 리소스의 예시로는 플레이어 캐릭터의 메시, 머터리얼, 텍스처, 애니메이션, 헤드업 디스플레이에 쓰이는 폰트, 텍스처 등이 있고 이들은 게임 내내 노출되는 리소스들이다.

 

게임의 특정 레벨의 수명을 따라가는 리소스들도 있다. 

 

게임속의 1회성 영상 (시네마틱)에 쓰이는 짧은 애니메이션과 오디오 클립은 영상이 시작하기 전에 미리 로드되었다가 영상 재생이 끝난 뒤 바로 메모리에서 내려가도록 만든다.

 

배경 음악이나 환경 사운드 효과, 풀 스크린 동영상 같은 리소스들은 실시간 스트리밍된다. 리소스를 구성하는 일부 바이트만 메모리에 존재하게 되는데 하드웨어가 한번에 읽어오는 4KiB와 같은 단위로 정하게 되며, 메모리에는 현재 재생 중인 청크와 다음 재생을 위해 메모리로 로드 중인 다음 청크, 두 개의 청크만 존재하게 된다.

 

리소스를 레벨의 수명을 따라가도록 하면 여러 레벨이 걸쳐 공유되는 리소스들은 계속 메모리에 올라갔다 내려갔다 반복하게 될 수 있는데, 리소스에 참조 카운터를 사용하게 되면 이런 문제를 막을 수 있다.


6.2.2.7 리소스와 관련된 메모리 관리

비디오 메모리에 있어야 하는 리소스들 (GPU가 빠르게 접근할 수 있도록)

ㄴ 텍스처, 정점 버퍼, 인덱스 버퍼, 셰이더 코드

 

글로벌 리소스와 실행 중 동적으로 오르는 리소스를 메모리 상의 서로 다른 장소에 두어 관리할 수도 있다.

 

리소스 매니저의 설계와 메모리 할당 하부 시스템의 설계는 밀접한 관계가 있다. 

메모리 할당자를 최대할 활용할 수 있게 리소스 매니저를 설계하는 경우도 있고 반대로 리소스 매니저가 가장 잘 활용할 수 있는 메모리 할당자를 설계하기도 한다.

리소스가 메모리에 올라갔다 내려갔다 하는 과정에서 메모리 단편화가 발생할 수 있는데 메모리 할당 시스템을 스택, 힙 기반으로 만들면 단편화 문제를 해결할 수 있다.

 

https://lemonyun.tistory.com/90

 

5. 엔진 지원 시스템

5.1 서브시스템 시작과 종료 게임 엔진은 서로 통신하는 수많은 하부 시스템이 모여 이루어진 복잡한 소프트웨어이다. 게임 엔진을 시작할 때 각 하부 시스템들을 정해진 순서에 따라 설정하고

lemonyun.tistory.com

힙 기반 리소스 할당

ㄴ 메모리 단편화를 무시하고 범용 힙 할당자 (new malloc)을 사용하는 방법, 가상 메모리 할당을 지원하는 운영체제 (PC) 에서는 메모리 단편화 문제가 덜하기 때문에 사용할 수 있다.

 

스택 기반 리소스 할당

ㄴ 게임 플레이 스타일이 특정한 경우에 사용 가능하다.

1. 게임이 선형 진행 방식이고 레벨 중심일 경우 (레벨1 - 로딩 - 레벨2 - 로딩 - 레벨3 이런 형식인 경우)

2. 레벨이 전부 메모리 안에 들어가는 경우 

 

풀 기반 리소스 할당

ㄴ 리소스 데이터를 똑같은 단위 크기의 청크로 불러오는 방법

ㄴ 모든 리소스 데이터가 똑같은 크기의 청크로 나누어질 수 있게 배열되어야 한다.

ㄴ 반드시 연속적으로 배열되어야 하는 데이터(배열, 구조체)들의 크기는 청크의 크기보다 작아야 한다. 

ㄴ 리소스 데이터를 설계할 때 청크로 나뉠 수 있도록 미리 고려하여 설계해야 한다. 배열과 같은 연속된 자료 구조 대신 연결 리스트를 사용하는 방법도 고려할 수 있다.

ㄴ 리소스 파일의 크기가 청크 크기의 배수가 되지 않는다면 파일의 마지막 청크에는 낭비되는 공간이 생길 수 있다는 단점이 있다.

ㄴ 각 청크의 크기는 I/O 버퍼의 배수가 되게 선택하여 청크를 메모리에 불러올 때 최적의 성능을 내도록 할 수 있다.

 

리소스 청크 할당자

ㄴ 청크에 쓰이는 메모리가 낭비되는 것을 줄이기 위한 방법으로 쓰이지 않는 공간이 있는 청크들에 대한 리스트와 각 청크에 있는 블록의 위치와 크기를 관리한다.

ㄴ 청크가 속한 레벨의 수명(이미 할당한 리소스의 수명)과 일치하는 메모리 요청(특정 레벨에 종속적인 데이터) 에만 리소스 청크 할당자를 사용하여 원하지 않을 때 리소스가 메모리에서 해제되는 현상을 방지한다.

 

6.2.2.8 복합 리소스와 참조 무결성

상호 의존하는 리소스들을 묶어 복합 리소스라고 부르기도 한다. (3D 모델 : 삼각형 메시, 뼈대, 애니메이션들의 리소스로 이루어짐)

대부분의 참조 관계는 의존 관계이다.

게임의 리소스 데이터베이스는 서로 의존하는 데이터 객체들로 이루어진 방향 그래프로 나타낼 수 있다.

 

데이터 객체끼리의 상호 참조는 내부 참조(같은 파일 안에 있는 두 객체 간의 참조)일 수도 있고 외부 참조(다른 파일에 있는 객체에 대한 참조)일 수도 있다.

 

6.2.2.9 리소스 간 상호 참조 처리

두 객체 간 상호 참조를 구현할 때는 포인터나 참조를 사용하지만 디스크 파일에 저장할 때는 포인터로 객체 간 의존성을 나타낼 수는 없다. (객체의 메모리 주소는 실행할 때마다 바뀔 수 있기 때문에)

 

1. GUID를 사용하는 방법

ㄴ 상호 참조 관계에 있을 수 있는 모든 리소스에 GUID를 부여한다.

ㄴ 런타임 리소스 매니저가 룩업 테이블을 전역적으로 관리해야 한다. (GUID와 객체에 대한 포인터를 키-값으로 갖는 테이블)

 

2. 포인터 교정 테이블 (여러 리소스 객체를 이진 파일로 저장한 뒤 메모리에 올려 사용하는 방법)

ㄴ 포인터를 파일 오프셋으로 변환하는 방법을 사용할 수 있다.

ㄴ 복합 리소스 내의 객체들을 이진 파일로 저장할 때, 파일에 각 객체에 있는 모든 포인터(다른 객체에 대한 참조)를 오프셋으로 대체하여 저장한다.

ㄴ 나중에 파일을 메모리에 불러올 때는 오프셋 값을 다시 포인터로 바꿔야 한다.

ㄴ 이진 파일에 포인터 교정 테이블(Fix-Up Table)을 둔다. 모든 객체내의 참조들의 위치를 기록한다. (파일에서는 오프셋으로 변경되어 있으므로 파일을 메모리에 올리려면 이 테이블에 있는 모든 위치의 오프셋을 포인터로 변경해줘야 한다.)

 

3. C++ 객체를 이진 파일로 저장하는 경우

ㄴ 객체의 생성자를 따로 불러줘야 함.

1. 그냥 생성자가 없는 C의 구조체나 가상함수가 없고 생성자에서 아무것도 하지 않는 데이터 타입(plain old data: PODS)만 이진 파일에 저장하는 방법

2. PODS가 아닌 객체들의 오프셋과 해당 객체들의 클래스 인스턴스 종류를 테이블에 같이 기록하고 placement new 문법(미리 할당된 메모리에 생성자를 호출하는 방법)을 사용하여 생성자를 호출하는 방법 

 

외부 참조 처리 (다른 리소스 파일안에 들어있는 객체를 참조하는 경우)

ㄴ 객체의 오프셋이나 GUID만으로는 표현할 수 없다. 객체가 담긴 리소스 파일에 대한 경로도 필요하다.

 

 

6.2.2.10 로드 후 초기화 과정, 정리 과정

리소스를 메모리기 올린 직후에 초기화 단계를 거치고 리소스의 메모리를 해제하기 전에는 정리 단계를 거친다.

리소스 타입마다 로드 후 초기화 및 정리 과정에서 할 일이 각기 다르다.

ㄴ C++ 에서는 다형성을 이용하여 클래스별로 다른 로드 후 초기화 및 정리 단계를 구현할 수도 있고 그냥 Init(), Destroy() 함수를 사용하여 구현할 수도 있다.

 

반드시 필요한 로드 후 초기화 과정 예시

ㄴ PC의 경우 3D 메시를 나타내는 정점과 인덱스 버퍼는 메모리에서 비디오 메모리로 옮겨져야 한다.

ㄴ 낡은 형식으로 된 메시 데이터를 최신 형식으로 변경하여 메모리의 최종 위치로 복사해야 하는 경우

 

보통 초기화 과정에서 새로운 데이터를 만들어 내기 때문에 리소스에 새로운 데이터를 저장하기 위한 여분의 메모리가 필요할 수 있다.

 

 

5.1 서브시스템 시작과 종료

게임 엔진은 서로 통신하는 수많은 하부 시스템이 모여 이루어진 복잡한 소프트웨어이다.

게임 엔진을 시작할 때 각 하부 시스템들을 정해진 순서에 따라 설정하고 초기화해야 한다.

 

5.1.1 C++ 정적 초기화 순서

C++에서는 프로그램의 시작 지점(entry point) 혹은 윈도우의 WinMain()이 호출되기 전에 전역 객체와 정적 객체를 생성한다. 전역 객체와 정적 객체들이 생성되는 순서는 임의적이다.

 

게임 엔진의 하부 시스템을 싱글턴 클래스로 디자인 할 수 없는 이유가 여기에 있다. 전역 혹은 정적 클래스 인스턴스를 생성하고 파괴하는 순서를 제어할 방법이 없기 때문이다.

 

5.1.1.1 주문형 생성 (좋지 않은 방법)

함수 안에서 선언된 정적 변수는  main() 실행 전이 아니라 해당 함수가 처음 호출될 때 생성된다.

그러므로 전역 싱글턴 객체를 함수의 정적 변수로 만들면 생성 순서를 제어할 수 있다.

객체의 파괴 순서는 제어할 수 없기 때문에 의존 관계가 있는 객체를 파괴할 때 문제가 생길 수 있다.

5.1.2 간단하고 제대로 된 방법

생성자와 파괴자를 사용하지 않고, 싱글턴 매니저 클래스에 매니저의 시작과 종료를 담당하는 함수를 명시적으로 정의하여 사용하는 방법이다.

class MemoryManager{
public:
    RenderManager() { // 아무것도 안함 }
    ~RenderManager() { // 아무것도 안함 }
    
    void startup() { // 매니저를 시작 }
    void shutdown() { // 매니저를 종료 }

}

MemoryManager gMemoryManager;
PhysicsManager gPhysicsManager;

int main () {
    gMemoryManager.startup();
    gPhysicsManager.startup();
    
    // 반대 순서로 종료함
    gPyhsicsManager.shutdown();
    gMemoryManager.shutdown();
}

엔진을 시작한다는 것은 단순히 싱글턴 클래스 순서를 정하는 것과는 다르다. 엔진 초기화 과정에서 다양한 운영체제 서비스 및 외부 라이브러리 등을 시작해야 한다.

 

5.2 메모리 관리

메모리가 성능에 영향을 끼치는 형태

1. 동적 메모리 할당, new malloc 연산자는 매우 느리기 때문에 동적 메모리 할당을 아예 피하거나 메모리 할당자를 직접 구현해 사용해야 한다.

2. 데이터를 연속적인 메모리 블록에 효율적으로 배치해야 CPU에서 처리하는 속도가 빠르다.

 

5.2.1 동적 메모리 할당 최적화

힙 할당자(malloc, new)가 느린 이유

1. 힙 할당자는 범용 목적이기 떄문에 관리하는 부가적인 비용이 들기 때문에 느리다.

2. 대부분의 운영체제에서는 힙 할당자를 사용할 때 context switching을 두 번 하기 때문에 (유저모드 커널모드 전환)

 

그렇기 때문에 힙 할당은 최소화하고, 타이트 루프 안에서는 절대 힙 할당을 하지 말아야 한다.

 

사용자 제작 할당자가 운영체제의 힙 할당자보다 대체적으로 성능이 뛰어난 이유

1. 사용자 제작 할당자는 미리 할당된 메모리 블록을 이용하기 때문에 처음에 메모리를 할당 받은 이후에는 context switching 비용을 지불하지 않아도 된다.

2. 사용 패턴을 예측할 수 있기 때문에 효율적으로 동작할 수 있다.

 

5.2.1.1 스택 기반 할당자

크고 연속적인 메모리 블록을 미리 할당(전역 변수 배열로 하는 방법 or malloc, new를 사용하는 방법)하고 스택의 꼭대기를 가리키는 포인터를 하나 유지한다.

스택의 특성상 임의의 순서로 메모리를 해제할 수 없도록 해야 하기 때문에 해제가 역순으로 수행되어야 한다. 이 원칙을 강제할 수 있는 단순한 방법은 개별 블록들이 아예 해제될 수 없게 만들고 스택의 꼭대기를 이전에 표시한 부분까지 롤백 하는 함수를 만들어 사용하는 방법이다.

 

5.2.1.2 풀 할당자

작은 메모리 블록을 같은 크기로 여러 개 할당하는 경우 사용 (같은 크기의 객체를 여러개 사용할 경우)

개별 원소들의 크기에 정확히 배수가 되는 큰 메모리 블록을 할당하는 것

사용 가능 리스트를 두고 메모리 할당 요청이 들어오면 리스트의 첫 원소가 가리키는 공간을 리턴한다. 사용 가능 리스트를 사용할 경우 각 원소마다 포인터 하나를 저장하는 것은 메모리 낭비이기 때문에 블록 자체에 다음 블록을 가리키는 포인터를 저장하는 방법을 사용하면 된다.

https://lemonyun.tistory.com/83

 

19. 최적화 패턴 - 객체 풀

객체를 매번 할당, 해제하지 않고 고정 크기 풀에 들어 있는 객체를 재사용함으로써 메모리 사용 성능을 개선한다. 19.2 동기 새로운 객체를 생성하고 제거하는 과정에서 발생하는 메모리 단편화

lemonyun.tistory.com

 

5.2.1.3 메모리 정렬 할당자

c++의 new가 메모리를 할당하는 방식이다.

메모리 정렬 할당자는 반드시 정렬된 메모리를 리턴할 수 있는 기능이 있어야 한다.

실제 요청된 것보다 조금 큰 메모리를 할당하고 블록의 주소를 살짝 위로 조정해서 정렬을 맞춘 다음 정렬된 주소를 리턴하면 된다.

 

5.2.1.4 단일 프레임과 이중 버퍼 메모리 할당자

 

단일 프레임 할당자

매 프레임마다 단일 프레임 할당자(스택 할당자의 형태로 구현)의 버퍼를 초기화한다. (할당한 메모리를 해제할 필요가 없다)

 

이중 버퍼 할당자

i번째 프레임에서 할당한 메모리 블록을 (i+1)번째 프레임에서 사용할 수 있게 해 주는 것

똑같은 크기의 단일 프레임 스택 할당자를 두 개 만들어 프레임마다 번걸아가면서 사용하는 것이다.

 

5.2.2 메모리 단편화

RAM에서 사용 가능한 메모리가 충분히 존재하지만 중간에 사용중인 공간이 있어서 연속적인 메모리 할당이 불가능한 상태를 메모리 단편화가 발생했다고 한다.

가상 메모리를 지원하는 운영체제의 경우 메모리 단편화가 큰 문제가 되지 않을 수도 있지만, 대부분의 콘솔 게임 엔진들은 가상 메모리가 갖는 본질적인 성능 저하 때문에 가상 메모리를 잘 활용하지 않는다.

 

5.2.2.1 스택 기반 할당자와 풀 할당자로 단편화 예방

스택 할당자는 언제나 연속적으로 할당되고 메모리 블록을 해제할 때는 할당 순서의 반대로 해야하기 때문에 메모리 단편화를 겪지 않는다.

 

풀 할당자는 모든 블록의 크기가 같기 때문에 연속된 공간이 부족해 할당이 실패하는 일은 없다.

 

5.2.2.2 조각모음과 재배치

크기가 제각각인 객체(풀 할당자를 사용할 수 없음)를 사용하거나 객체들이 순서 없이 할당됐다가 해제되는 경우(스택 기반 할당자를 사용할 수 없음)에는 단편화 문제를 해결하기 위해서 힙을 주기적으로 조각 모음(defragmentation)해야 한다.

 

이미 할당되어 사용되고 있는 메모리 블록들을 주소가 낮은 쪽의 구멍을 메우면서 옮겨야 하기 때문에 메모리 블록을 가리키는 포인터들을 모두 찾아 새로운 주소를 가리키도록 포인터 재배치를 해야 한다.

 

포인터 재배치를 위해 포인터들을 일일이 관리하는 방법을 사용할 수도 있고 포인터 대신 스마트 포인터 혹은 핸들을 사용하여 관리하는 방법도 있다.

스마트 포인터를 사용하는 경우 모든 메모리 블록을 가리키는 스마트 포인터들을 전역 연결 리스트에 추가하여 관리한다.

힙에서 어떤 블록을 재배치 하는 경우, 연결 리스트의 모든 스마트 포인터를 검색해서 재배치되는 블록을 가리키는 포인터를 새로운 주소로 업데이트한다.

 

조각 모음으로 인한 성능 저하를 분산하기 위해 힙 전체를 한꺼번에 조각 모음하지 않고 여러 프레임에 걸쳐 진행할 수 있다. 이런 접근 방식이 동작하려면 각 블록의 크기가 상대적으로 작아서 블록을 이동하는 시간이 한 프레임에서 할당된 시간을 초과하지 않아야 한다.

 

5.3 컨테이너

배열 : 순서가 있는 연속적인 요소들에 인덱스로 접근한다. 컴파일할 때 길이가 정적으로 결정된다.

동적 배열 (std::vector) : 런타임에 길이가 변할 수 있는 배열 (연속적인 메모리에 저장)

연결 리스트 (std::list) : 순서가 있는 요소들의 모음이지만 메모리에 연속적으로 저장되는 것이 아니라 포인터에 의해 요소들이 연결된다.

스택 (std::stack) 

큐 (std::queue)

덱 (std::deque)

트리 : 요소들이 계층 구조로 구분된 컨테이너이다.

이진 검색 트리 : 각 노드는 최대 두 개의 자식을 가지고 자식들이 속성에 따라 명확한 기준에 의해 정렬되는 트리

이진 힙 : 이진 트리, 말단 노드들은 왼쪽에서 오른쪽으로 채워져 있어야 하며, 모든 노드는 사용자가 정의한 기준에 따라 그 자식들보다 크거나 같아야 한다.

우선순위 큐 (std::priority_queue) : 언제나 정렬되어 있는 리스트, 항상 최고 우선순위의 요소만을 꺼낼 수 있다. 보통 힙으로 구현한다.

맵, 해쉬맵 (std::map, std::hash_map) : 키-값 쌍으로 이뤄진 테이블 

집합 : 정해진 기준에 의해 중복되는 요소가 없게 보장하는 컨테이너

그래프 : 노드들의 집합으로 노드 간에 단방향 혹은 양방향으로 연결돼 임의의 패턴을 이룬다.

방향성 비순환 그래프 : 단방향으로 연결된 노드들의 집합

 

5.3.2 반복자

특정한 컨테이너의 요소들을 효율적으로 접근하는 방법을 알고 있는 작은 클래스를 반복자라고 한다.

컨테이너의 요소들에 직접 접근하는 대신 반복자를 사용할 때 얻는 이점

ㄴ 컨테이너 클래스의 캡슐화를 지키며(컨테이너의 내부 구현 세부 사항을 외부에 노출하지 않고도) 효율적으로 순회할 수 있다.

ㄴ 반복자는 순회 과정을 단순화 한다. (트리를 순회 하는 경우 그냥 반복자를 증가시키면 끝)

 

5.3.4 자체 구현 컨테이너 클래스 만들기

컨테이너 클래스를 직접 구현하는 이유

ㄴ 목표로 하는 콘솔의 하드웨어 특성에 맞게 알고리즘을 최적화할 수 있다.

ㄴ 외부 의존성을 제거할 수 있다 - 외부 라이브러리를 사용하는 경우 라이브러리에 문제가 생겼을 때 빠르게 대응할 수 없다.

ㄴ 외부 라이브러리에 잘 없는 알고리즘을 자체적으로 만들어 넣을 수도 있다.

ㄴ 병행 데이터 구조에 대한 제어가 가능하다.

 

5.3.4.1 컨테이너를 어떻게 구현할 것인가?

1. 필요한 자료 구조를 직접 만든다. (자체 구현)

2. 외부 구현을 사용한다.

ㄴ STL, STLport (C++ 표준 템플릿 라이브러리)

ㄴ Boost 라이브러리

 

STL의 장점 

ㄴ 다양한 플랫폼에서 쓸 수 있는 대체적으로 안정적인 구현들이 존재한다.

ㄴ 거의 모든 C++ 컴파일러는 STL을 표준으로서 지원한다.

 

STL의 단점

ㄴ 대부분 자체 제작한 자료 구조보다 메모리를 더 많이 소비한다.

ㄴ 어떤 문제를 해결하기 위해 구체적인 목적을 가지고 제작한 자료 구조와 비교할 때 STL의 자료구조는 이보다는 성능이 떨어진다.

ㄴ 동적 메모리 할당을 많이 사용하기 때문에 고성능이 필요하다. 고성능 CPU와 가상 메모리 시스템이 있는 PC 플랫폼에서는 메모리 할당이 다른 플랫폼에 비해 제약이 덜하지만 그런 조건이 없는 콘솔 플랫폼에서는 적합하지 않을 수 있다.

 

STL을 사용할 때 고려해야 할 점

ㄴ 사용하는 STL 컨테이너의 성능 및 메모리 특성을 알고 있어야 한다.

ㄴ 성능이 병목될 만한 코드에는 크고 무거운 STL 클래스들의 사용을 자제해야 한다.

ㄴ 엔진이 여러 플랫폼을 지원하는 경우 STLport를 사용하자

 

Boost 

ㄴ STL을 확장하는 동시에 STL과 같이 쓰일 수 있는 라이브러리를 만들어 내는 오픈소스 프로젝트

ㄴ 문서화가 잘 되어 있다.

ㄴ 스마트 포인터 등 복잡한 문제를 훌륭하게 처리한다.

 

Loki 

ㄴ C++의 템플릿을 이용해 원래는 런타임에 해야 할 일들을 컴파일러가 하게 만드는 라이브러리

ㄴ 읽고 사용하기 힘들며 컴파일러의 부수 효과에 의존하기 때문에 다른 라이브러리들에 비해 이식성이 떨어진다.

 

5.3.4.2 동적 배열과 메모리 할당

게임 프로그래밍에서는 C 형태의 고정 크기 배열을 많이 사용한다.

ㄴ 동적 메모리 할당이 필요 없다.

ㄴ 메모리가 연속적이어서 캐시 성능이 좋다.

ㄴ 데이터 추가나 검색 등 자주 쓰는 동작을 효율적으로 할 수 있다.

 

동적 배열 (std::vector)의 작동 방식

ㄴ 처음에 n개의 요소를 담을 버퍼를 할당한 후 공간이 더 필요한 경우 버퍼를 키운다.

ㄴ 새로운 더 큰 버퍼를 동적 할당하고 원래의 버퍼에서 새 버퍼로 데이터를 복사하는 방식

ㄴ 동적 배열은 사용될 버퍼의 크기를 아직 정하지 못한 개발 기간에 사용하고 메모리 사용량이 정해진 이후에는 고정 크기의 배열로 언제든 바꿀 수 있다.

 

5.3.4.3. 연결 리스트

연속적인 메모리를 마련하는 것보다 임의의 위치에 요소를 삽입하고 제거하는 동작이 더 중요한 경우에는 연결 리스트를 사용하는 것이 낫다.

 

연결 리스트를 구현하는 두 가지 방법

1. extrusive list (돌출 리스트)

ㄴ 링크 자료 구조와 요소 자료구조가 완전히 별개인 연결 리스트

ㄴ 한 요소가 동시에 여러 연결 리스트에서 포함할 수 있다는 장점

ㄴ 링크 객체를 동적 할당해야 한다는 단점이 있기 때문에 풀 할당자를 사용할 여력이 있다면 괜찮은 방법이다.

ㄴ 외부 라이브러리의 클래스 인스턴스를 연결 리스트에 넣어야 하는데 라이브러리의 소스코드를 수정할 수 없다면 선택해야 하는 방법

 

2. instrusive list (함몰 리스트)

ㄴ 요소 자료구조 안에 링크 자료구조를 포함시키는 연결 리스트

ㄴ 링크 객체를 동적 할당하지 않아도 된다.

ㄴ 요소 클래스가 링크 클래스를 상속받게 구현하는 방법도 있다.

 

5.3.4.4. 사전과 해쉬 테이블 (map, hashmap)

ㄴ 키가 주어지면 값을 빠르게 찾아주는 자료구조, 이진 검색트리나 해쉬 테이블을 사용한다.

 

해쉬 테이블 

키를 정수형태로 변환(hashing)하고 변환된 값을 테이블 크기로 모듈로 연산을 하여 테이블 인덱스를 계산한다.

 

개방형 해쉬 테이블

ㄴ 충돌을 해결하기 위해 인덱스 하나에 여러 개 의 키-값 쌍을 연결 리스트의 형태로 저장하는 방식을 사용

ㄴ 새로운 키-값 쌍을 테이블에 추가할 때 동적 메모리 할당이 발생

 

폐쇄형 해쉬 테이블

ㄴ 충돌이 발생하면 빈 슬롯을 찾을 때까지 탐지(정의된 알고리즘을 사용하여 빈 슬롯을 찾는 과정)을 반복한다.

ㄴ 구현이 까다롭고 테이블에 저장할 수 잇는 최대 키-값 쌍의 수에 제한이 있지만, 정해진 메모리만 사용하고, 동적 메모리 할당이 필요가 없다는 장점이 있다.

ㄴ 구현 시 선형 탐지, 이차 탐지 알고리즘을 사용할 수 있다. 테이블의 크기를 소수가 되게 하는것이 좋다.

 

해시 함수

ㄴ 키가 32비트 정수인 경우 변환값은 그대로이다. 

ㄴ 32비트 부동소수의 경우 변환값은 비트값 그대로이다. (32비트 정수로 취급)

ㄴ 문자열의 경우 모든 문자의 ASCII 코드나 UTF 코드를 모아서 32비트 정수 값 하나로 변환한다.

 

좋은 해시 함수는 충돌을 최소화하도록 키 값들을 테이블 전체에 고르게 배분하는 함수이다.

 

해시 함수의 예

LOOKUP3

Cyclic redundancy check function

MD5

 

5.4 문자열

5.4.1 문자열의 문제점

어떤 타입을 사용할 것인가?

ㄴ 문자열 클래스

ㄴ character(문자)의 배열

 

localization(국제화)에 관한 문제

ㄴ 플레이어가 볼 수 있는 문자열을 지역에 따라 번역해야 한다.

 

런타임에 문자열을 처리하는 것은 보통 느리다

ㄴ 예를 들어 문자열 비교, 복사는 int나 float의 비교 복사보다 훨씬 느리다

 

5.4.3 고유 식별자

가상 게임 월드에 있는 물체들을 고유하게 구분할 방법으로 문자열을 사용하는 것은 자연스러운 선택이지만, 고유 식별자들 끼리 비교를 하는 경우 성능상의 문제가 발생할 수 있기 때문에 다른 방법을 사용할 수 있다.

 

5.4.3.1 해시 문자열 ID

고유 식별자들 끼리 비교를 할 때 해시 테이블에 문자열과 정수 ID를 저장하고 비교는 정수 ID로 한다.

 

문자열에서 문자열 ID를 만드는 과정을 문자열을 인턴한다고 부른다.

문자열 인턴은 시간이 오래걸리기 때문에 초기에 한 번만 문자열을 인턴하고 그 결과를 나중에 쓸 수 있게 저장하는 것이 좋다.

 

5.4.4 현지화 

5.4.4.1 유니코드

 

UTF-32

ㄴ 문자열의 모든  character가 4바이트를 차지함(고정 크기)

 

UTF-8

ㄴ 문자열의 캐릭터가 1바이트 혹은 여러 바이트를 차지할 수 있다.

ㄴ ANSI 인코딩과 하위 호환성이 있다.

 

UTF-16 (wide character set) 

ㄴ 유니코드 코드포인트의 집합(플레인) 은 17개이다. 기본 다중언어 플레인(bmp, basic multilingual plane)1개와 보조 플레인 16개가 있다. 각 플레인은 2의 16승 (65536)개의 코드 포인트를 가진다.

ㄴ 문자열의 캐릭터 하나는 최소 2바이트 (4바이트가 될 수도 있음)

ㄴ 기본 다중언어 플레인만을 활용하는 경우 2바이트, 캐릭터가 다른 플레인에서 온 경우 4바이트

ㄴ 대상 CPU에 따라 little-endian 혹은 big-endian이 될 수 있기 때문에 디스크에 저장하는 경우 BOM(바이트 순서 마크)를 지정 해야 한다.

ㄴ 한글은 UTF-8에서 3바이트로 변환되지만 UTF-16에서는 2바이트로 변환되므로 효율적이다.

 

UCS-2 

ㄴ 기본 다중언어 플레인만을 활용하는 UTF-16 인코딩의 제한된 부분집합

ㄴ 유니코드 코드포인트가 수치적으로 0xFFFF (65535) 보다 큰 캐릭터를 표현할 수 없다.

ㄴ 문자열의 캐릭터 하나는 2바이트 고정

 

 

5.4.4.2 char 타입과 wchar_t 타입

 

5.4.4.3 윈도우즈 환경에서 유니코드

유니코드 = wide character set = UTF-16로 간주한다.

 

std::string은 STL의 ANSI 문자열 클래스

std::wstring은 와이드 캐릭터 클래스 (UTF-16 인코딩된 문자열을 위해 사용)

 

char *s = "this is a string" 

wchar_t * s = L"this is a string"

 

5.5 게임 엔진 설정

그래픽 품질, 사운드 효과, 컨트롤러 설정과 같은 게임 엔진의 옵션을 설정하는 방법

 

5.5.1 옵션 불러오기와 저장하기

텍스트 설정 파일

ㄴ 윈도우의 INI 파일은 논리적 단위로 구분된 단순한 키-값 쌍으로 이뤄져 있다.

 

윈도우 레지스트리 

ㄴ 잘 조직된 INI 파일들의 모음과 같은 역할을 한다. 윈도우 애플리케이션이 사용하는 INI 파일이 점점 복잡해지는 문제를 해결하기 위해 레지스트리를 사용한다.

 

커맨드라인 옵션

ㄴ 엔진의 모든 옵션을 커맨드라인으로 설정할 수 있는 경우도 있다.

 

온라인 유저 프로파일

ㄴ 사용자마다 자신이 구매한 게임, 획득한 업적, 게임 옵션 등 여러 정보들이 중앙 서버에 저장되고 인터넷을 통해 접근하는 방식

5.5.2 사용자별 정보

대부분의 게임 엔진에서는 전역 옵션과 사용자별 옵션을 구분한다.

ㄴ 플레이어의 기호에 맞게 게임을 설정할 수 있다. (플레이어 관점의 사용자별 옵션)

ㄴ 프로그래머나 아티스트, 디자이너가 다른 팀원들에 영향을 주지 않으면서 자신만의 개발 환경을 마련할 수 있다.

 

DirectX 12 공부하면서 다뤘던 내용들이라 헷갈렸던 내용만 다시 정리한다.

 

4.4 사원수

https://lemonyun.tistory.com/63

 

22. 사원수 (quaternion)

사원수를 쓰는 이유 x y z 축을 순서에 따라 회전시키게 되면 축이 겹치게 되는 현상(gimbal lock)이 생기기 때문 복소수와 사원수 사원수 곱셈은 교환법칙을 만족하지 않지만 결합법칙은 만족한다.

lemonyun.tistory.com

 

4.7 하드웨어 가속 SIMD

비주얼 스튜디오 컴파일러는 __m128이라는 데이터 타입을 지원한다.

__m128 타입 변수는 일반적으로 RAM에 저장된다. 하지만 연산에 사용될 때는 CPU의 SSE 레지스터에서 처리된다.

실제로 지역 변수나 함수 인자를 __m128 타입으로 선언하면 컴파일러가 이 값을 프로그램 스택에 저장하기보다는 바로 SSE 레지스터에 저장하는 경우가 많다.

 

4.7.2.1 __m128 변수의 정렬

__m128 타입의 변수가 RAM에 저장될 때 프로그래머는 이 변수가 16바이트 메모리 주소 경계에 위치하게 보장해야 된다.

__m128 변수를 한 개라도 포함하고 있는 구조체나 클래스를 자동 혹은 전역 변수로 선언하면 컴파일러가 알아서 정렬한다. 하지만 동적으로 할당된 메모리에 있는 자료구조의 정렬에 대해서는 프로그래머가 책임져야 한다.

 

4.7.3 SSE 내장 명령어로 코드 짜기

컴파일러가 지원하는 내장 명령어(외관이나 사용법은 일반적인 C 함수와 같지만 컴파일러가 알아서 인라인 어셈블리 코드로 바꿔주는 명령어)를 사용하여 SSE 연산을 할 수 있다.

 

4.8 난수 생성

4.8.1 선형 합동 생성기

ㄴ 처음 주어진 시드 값이 같으면 생성되는 숫자들은 정확히 똑같다.

 

4.8.2 메르센 트위스터

ㄴ 선형 합동 생성기의 여러 단점을 개선하기 위해 고안된 유사난수 생성 알고리즘,

SIMD 벡터 명령을 사용해 속도를 높인 SFMT(SIMD-oriented fast mersenne twister)를 많이 사용한다.

'읽은 책 > 게임 엔진 아키텍처' 카테고리의 다른 글

6. 리소스 시스템과 파일 시스템  (0) 2022.08.08
5. 엔진 지원 시스템  (0) 2022.08.06
3. 게임을 위한 소프트웨어 엔지니어링 기초  (0) 2022.08.03
2. 도구  (0) 2022.07.31
1. 소개  (0) 2022.07.30

 

3.1 C++ 개념과 올바른 사용법

3.1.1.1 클래스와 객체

클래스와 인스턴스(객체)는 일대다 관계이다.

 

3.1.1.2 캡슐화

클래스를 사용하는 프로그래머의 입장에서는 정해진 인터페이스만 잘 이해하면 구현을 어떻게 하든 신경 쓰지 않아도 된다.

클래스를 만드는 프로그래머의 입장에서는 클래스 인스턴스가 항상 논리적으로 일관된 상태를 유지하게 보장할 수 있다. (클래스 외부에서는 정해진 인터페이스만 사용하고 내부의 데이터를 건드릴 수 없기 때문에 외부에 의한 간섭이 없다)

 

3.1.1.3 상속

상속은 두 클래스 사이에 is-a 관계를 만든다.

C++에서는 다중 상속을 잘 사용하지 않는다. 단일 상속만을 기본으로 하는 구조를 유지하되 간단하고 부모를 갖지 않는 클래스만 추가해 다중 상속을 사용하는 것이 정석이다. 이런 클래스를 믹스인 클래스라고 부른다.

 

3.1.1.4 다형성

서로 다른 타입의 객체들을 하나의 공통 인터페이스로 다룰 수 있는 기능이다.

 

3.1.1.5 합성과 집합

합성(composition) : 서로 영향을 주고받는 여러 객체들을 이용해 복잡한 일을 해결하는 것

ㄴ 클래스 간에 has-a 관계나 uses-a 관계를 형성한다.

ㄴ 다른 클래스의 인스턴스를 멤버로 포함하게 설계하는 방식이다.

 

3.1.1.6 디자인 패턴

싱글턴

반복자

추상화 팩토리

 

3.1.2 코딩 규칙

1. 인터페이스를 중시할 것 : 인터페이스(.h 파일)는 간결하고 단순하며 최소한의 것만 포함해야 한다.

이름을 잘 지을 것

2. 전역 네임스페이스를 깔끔하게 유지할 것 : 다른 라이브러리의 이름과 충돌하는 것을 막아야 한다.

3. 널리 알려진 C++ 사용법을 따를 것 : (Effective C++를 읽기)

 

3.1.3 C++11

3.1.3.1 auto

auto : 컴파일러가 오른쪽 값으로부터 타입을 추정한다.

auto f = 3.141592f; 

 

3.1.3.2 nullptr

기존에는 NULL은 정수값 0과 동일해서 int 값과 비교가 가능했지만 nullptr은 비교가 불가능하다.

std::nullptr_t 타입의 인스턴스인 nullptr를 NULL 포인터로 사용한다.

 

3.1.3.3 영역 기반 for 루프

C-스타일 배열과 멤버가 아닌 begin()과 end()함수가 정의되는 다른 자료구조에 대해 반복 작업을 수행할 수 있다.

for (const auto& pair : myMap) {  } 

 

3.1.3.4 override와 final

C++11 이전에는 virtual 키워드 하나만 사용할 수 있었음

final : 파생 클래스에 의해 오버라이딩되지 않아야 하는 단말(leaf) 가상 함수를 구현할 때

override : 상속받은 가상 함수를 오버라이딩할 때

 

3.1.3.5 강한 타입의 enum

enum 클래스를 사용하여 스코프 문제와 enum 값이 int 변수 값과 비교될 수 있는 문제를 없앴다.

 

3.1.3.6 표준화된 스마트 포인터

std::unique_ptr : 가리키고 있는 객체에 대해 단독 소유권을 유지하기 원할 때 사용

std::shared_ptr : 단일 객체에 대해 다수의 포인터를 사용하고 싶을 때 (참조 횟수로 관리)

std::weak_ptr :  shared_ptr처럼 작동하지만 객체를 가리키는 참조 횟수에는 기여하지 않는다.

 

3.1.3.7 람다

labmda는 익명 함수이다. 펑터(함수처럼 동작하는 클래스), 또는 std::function (함수 포인터로 사용)이 사용되는 모든 곳에 사용할 수 있다.

 

3.1.3.8 move 의미와 rvalue 레퍼런스

C나 C++에서 lvalue는 컴퓨터의 레지스터나 메모리에 있는 실제 저장 위치를 나타낸다. rvalue는 일시적인 자료 객체로서 논리적으로 존재하는 것이지 반드시 메모리를 차지하고 있을 필요는 없다.

 

이동 생성 연산자를 정의하면 move()함수를 사용하여 rvalue 복사를 할 수 있다.

 

lvalue 레퍼런스는 [자료형]& , rvalue 레퍼런스는 [자료형]&& 을 통해 선언된다.

 

어떤 함수에서 임시 객체를 생성하여 객체를 반환할 때 함수 외부에서 반환 값을 받는 부분의 경우는 깊은 복사가 필요 없는데 깊은 복사(복사 생성자에 의해)가 일어난다. 이동 생성자를 정의하면 객체를 새로 동적 할당 하지 않고 대입할 수 있다.

 

3.2 데이터, 코드와 메모리

3.2.1 수 표현

3.2.1.4 부동소수점 표현법

부동소수의 정확도는 절대값이 작을수록 높아진다. - 한전된 비트로 표현된 가수를 정수부와 소수부가 나눠 써야하기 때문이다.

0과 부동소수점으로 표현할 수 있는 0이 아닌 가장 작은 수(음수 제외)는 분명한 간격이 있다.

machine epsilon : 23비트 정확도(가수)를 지닌 IEEE-754 부동소수점(부호1, 지수8, 가수 23)에서 머신 엡실론은 2의 -23승이다. 

머신 엡실론 보다 작은 수를 연산에 사용하면 잘려 나간다. 머신 엡실론을 1.0에 더한 결과는 1.0이다. 

 

3.2.1.5 기본적인 데이터 타입

기본 타입

ㄴ int, char, short, long, double, bool

 

컴파일러에 따라 크기가 다른 타입

ㄴ int8, __int16, __int32, __int64

 

SIMD 타입

ㄴ 요즘의 CPU에는 벡터 프로세서, 혹은 벡터 유닛이라 불리는 산술 연산장치가 있다.

ㄴ 128비트 SIMD 레지스터에 부동소수 4개(32비트 4개) 를 넣어 쓰는 것이 흔하다. 행렬 곱셈이나 내적을 ALU보다 빠르게 처리할 수 있다.

각 마이크로프로세서마다 서로 다른 SIMD 명령어 셋을 구현하고 컴파일러마다 SIMD 변수를 선언하는 방법도 제 각각이다.

 

이식 가능한 크기 타입

컴파일러마다 서로 데이터 타입의 크기가 다르고 살짝 다른 문법을 사용하기 때문에 대부분의 게임 엔진은 스스로 내장 데이터 타입을 정의해서 이식 가능한 코드를 만든다.

C++11 표준라이브러리는 표준화된 크기의 정수 타입을 제공한다. 이것들은 <cstdint>헤더에 선언되어 있다.

std::int8_t, std::int16_t, std::int32_t, std::int64_t를 사용할 수 있다.

 

3.2.1.6 멀티바이트 데이터와 엔디언

1바이트보다 큰 값을 멀티바이트 값이라고 한다.

little-endian : 데이터의 하위 바이트가 낮은 메모리 주소에 저장되는 방식

big-endian : 데이터의 상위 바이트가 낮은 메모리 주소에 저장되는 방식

 

개발할때의 프로세서가 little-endian 방식인데 게임 실행은 big-endian에서 하는 경우 문제가 될 수 있다.

해결 방법

1. 모든 데이터 파일을 텍스트 형태(10진수)로 저장하고 숫자 하나당 바이트 하나씩 사용하여 저장

2. 툴에서 디스크에 저장하기 직전에 엔디언을 바꾸게 하는 방법

 

정수의 엔디언 바꾸기

ㄴ MSB에서 시작해서 LSB와 값을 1바이트 단위로 바꾼다. 가운데에 도달할때 까지 반복하면 된다.

 

부동소수의 엔디언 바꾸기

ㄴ 정수인 것처럼 생각하고 바꾸면 된다. 유니온을 사용하면 쉽다.

 

3.2.2 선언, 정의, 연결성

3.2.2.1 번역 단위 다시 살펴보기

cpp 파일 한 개(번역 단위)를 번역하면 목적 파일 한 개가 생긴다.

목적 파일에는 정의된 함수를 번역한 기계어, 전역 변수와 정적 변수,  다른 .cpp 파일에서 정의된 함수를 가리키는 미확정 참조를 담고 있다.

 

목적 파일들을 모두 모아서 완성된 실행 파일로 만드는 것은 링커의 몫이다. 이 과정에서 링커는 모든 목적 파일을 읽어 미확정 상태인 외부 참조가 진짜 어떤 것인지 알아내려고 시도한다.

 

링커의 주된 역할은 외부 참조를 해결하는 일인데, 이와 관련해 링커가 낼 수 있는 에러는 두 가지이다.

1. extern으로 선언된 외부 참조를 찾아낼 수 없는 경우, 미확정 심볼 (unresolved-symbol) 에러

2. extern으로 선언된 외부 참조를 두 개 이상 발견한 경우, 중복 정의된 심볼 (multiply-defined symbol) 에러

 

3.2.2.2 선언과 정의의 차이 (+ 인라인 함수)

선언

ㄴ 데이터 객체나 함수의 형태를 나타낸다. 이름과 데이터 타입 혹은 함수의 서명(리턴 타입과 인자 타입)을 알려준다.

정의

ㄴ 프로그램 안에 고유한 저장 공간을 나타낸다. 

 

선언으로 함수를 표현하는 경우 다른 번역 단위에서 참조하거나 같은 번역 단위에서 사용할 수 있다. 함수 서명 후에 세미콜론을 붙이면 선언이 되는데 extern 키워드를 붙여도 되고 붙이지 않아도 된다. (안붙이면 붙어있는 것으로 처리됨)

다른 번역 단위에서 정의된 전역 변수를 사용할 때는 현재 번역 단위에서 extern 키워드를 앞에 붙여 선언하면 된다.

 

헤더 파일에 정의를 두지 않는 이유

ㄴ 여러 .cpp 파일에서 #include 구문으로 정의를 불러들이면 중복 정의된 심볼 에러가 발생하기 때문에

ㄴ 인라인 함수의 정의는 예외이다.

 

컴파일러가 인라인 함수를 처리하려면 컴파일러가 함수 구현을 볼 수 있어야 한다.

ㄴ 헤더 파일에 inline 키워드를 붙인 함수를 정의한다.

ㄴ 인라인 키워드는 컴파일러에게 주는 힌트일 뿐이다. 인라인 할지 안할지는 컴파일러가 함수 크기와 얻을 수 있는 효율성을 분석해 정한다.

ㄴ 인라인 함수를 호출하는 부분은 함수 자체의 내용 복사본으로 대체되어 함수 호출 오버헤드가 제거된다.

 

3.2.2.3 연결성

C/C++의 모든 정의마다 연결성(linkage)이라는 속성이 있다.

기본적으로 모든 정의는 외부 연결성(external linkage)이다.

static 키워드는 정의를 내부 연결성으로 바꿀 때 사용한다.

ㄴ static 함수를 예로 들면 다른 번역 단위에 동일한 이름의 함수가 있어도 중복 정의된 심볼 에러를 내지 않고 자신의 번역 단위에 있는 static 함수를 사용한다.

 

3.2.3 C/C++ 프로그램의 메모리 구조

 

3.2.3.1 실행 파일 이미지 (= 프로그램 코드, 데이터)

1. 텍스트 세그먼트 = 코드 세그먼트 = 기계어

2. 데이터 세그먼트 = 전역 변수, 정적 변수 

3. 읽기 전용 데이터 세그먼트 = const로 선언된 전역 변수, 인스턴스

 

3.2.3.2 프로그램 스택

실행 파일이 메모리에 로드될 때 운영체제는 프로그램 스택이라는 메모리 공간을 마련한다.

함수가 불릴 때마다 스택 프레임이라는 것을 프로그램 스택에 push하고 리턴할 때마다 pop한다.

 

스택 프레임에는 세 가지 종류의 데이터가 저장된다.

1. 리턴 주소

ㄴ 함수 리턴 후 PC레지스터가 가리켜야 할 주소(함수를 호출했던 곳)

 

2. CPU 레지스터

ㄴ 함수가 호출되기 이전의 상태로 돌아가기 위해 이전 레지스터들의 값들을 저장하고 있어야 한다.

 

3. 지역 변수

ㄴ 호출된 함수의 지역 변수들은 해당 함수 스택 프레임에 존재한다.

 

3.2.3.3. 동적 할당 힙

전역 변수, 정적 변수, 지역 변수 등은 정적(static)으로 정의되는 저장 공간이어서 컴파일할 때 저장 공간을 알 수 있지만, 프로그램이 메모리를 얼마나 사용할지 컴파일할 때 알 수 없는 경우 프로그램은 추가로 더 필요한 메모리를 동적으로 할당해야 한다.

C++에서는 new delete 연산자를 사용한다.

 

3.2.4 멤버 변수 

클래스나 구조체의 선언만으로는 어떠한 저장 공간도 할당하지 않는다. (멤버 변수들을 위한 저장 공간은 없다)

 

3.2.4.1 클래스 정적 멤버

클래스 정적 변수는 자동으로 클래스의 네임스페이스에 포함되기 때문에 클래스 밖에서 지칭할 때는 항상 클래스 이름을 사용해야 한다. ([클래스 이름]::[변수 이름])

 

3.2.5 메모리상의 객체 구조

 

3.2.5.1 메모리 정렬과 패킹

요즘 나오는 CPU 상당수가 메모리 정렬이 제대로 지켜진 데이터 블록만 읽고 쓸 수 있다.

메모리 정렬 조건 : 객체가 저장된 메모리 주소가 객체의 크기의 배수여야 한다.

구조체와 클래스의 멤버의 정렬 조건 중 가장 큰 정렬 조건을 가지는 멤버(크기가 가장 큰 멤버)의 정렬 조건이 곧 구조체의 정렬 조건이 된다.

구조체 내의 데이터 멤버를 어느 순서로 선언하느냐에 따라 메모리 효율성이 달라질 수 있다.

구조체의 멤버로 패딩을 넣는 것은 구조체가 배열로 사용되었을 경우에도 메모리 정렬을 보장하기 위함이다.

 

3.2.5.2 C++ 클래스의 메모리 구조

컴파일러는 베이스 클래스나 클래스에 가상함수가 있는 경우 가상함수 테이블을 만들고 테이블을 가르키는 4바이트 포인터(64비트 환경에서는 8바이트) 를 클래스 인스턴스 마다 둔다.

 

class A {
    int x;
    int y;
    
    virtual void Draw() {
    
    }
}

class B : public A {
    int x;
    int y;
    
    virtual void Draw() {
    
    }
}

B 클래스 인스턴스의 메모리 구조

sizeof(B)는 20이 된다.

3.2.6 킬로바이트와 키비바이트

킬로바이트 = 1000 바이트

키비바이트 (KiB) = 1024 바이트

 

3.3 에러 감지와 처리

사용자 에러와 프로그래머 에러로 구분할 수 있다.

 

3.3.2 에러 처리

1. 사용자가 게임 플레이어인 경우

ㄴ 게임 로직의 에러는 게임의 내용에 맞게 처리되는 것이 자연스럽다. (게임의 규칙을 따르게 한다.)

 

2. 사용자가 아티스트나 애니메이터, 게임 디자이너 등 게임을 만드는 다른 사람일 경우

ㄴ 잘못된 게임 자원을 사용할 경우 발생한다.

ㄴ 에러가 발생하더라도 다른 자원으로 대체하여 계속 진행할 수 있도록 하는 유연함이 필요하다.

 

3. 프로그래머 에러 처리

ㄴ 가장 좋은 방법은 에러 감지 코드를 소스코드 곳곳에 넣고 감지가 되면 시스템을 멈춰버리는 assertion 시스템을 활용하는 것

 

3.3.3 에러 감지와 에러 처리 구현

1. 에러 리턴 코드

ㄴ 함수의 리턴 값으로 불가능한 값을 리턴하여 에러를 처리하는 방법

ㄴ 에러를 감지한 함수가 그 에러를 다룰 수 있는 함수와 연관이 없는 경우 에러 코드를 에러를 다룰 수 있는 상위 함수에 도달할 때까지 전달해야 한다. 

 

2. 예외 처리

ㄴ 에러가 발생했을 때 어떤 함수가 처리할 것인지 신경쓰지 않고 에러를 전달할 수 있다.

ㄴ 프로그래머가 정의한 예외 객체에 연관 정보를 넣고 콜 스택을 펼쳐(펼치는 과정에서 지역 변수들은 소멸된다.)  try-catch 블록에 도달할 때까지 반복되며 찾으면 catch 블록 안의 코드가 실행된다.

ㄴ 별로 안좋다고 한다. 

 

3. assertion

ㄴ C++ 에서는 cassert 헤더 파일을 include하고 assert(표현식) 의 형태로 사용한다.

ㄴ assertion은 #define 매크로로 구현되기 때문에 #define 구문만 바꿔주면 assertion을 없어지게 할 수도 있다. 

ㄴ assertion이 실패할 경우 항상 프로그램 실행을 멈춰야 한다.

ㄴ 치명적인 에러를 잡는데만 써야 한다.

ㄴ 사용자 에러를 찾는데 써서는 안된다.

 

3.4 파이프라인, 캐쉬 그리고 최적화

여러가지 성능 최적화 중 여기에서는 소프트웨어가 계산을 최대한 빠르게 수행하도록 만드는 방법에 대해서 논한다.

 

3.4.1 병렬처리 패러다임의 변화

과거에는 CPU가 상대적으로 느렸기 때문에 프로그래머는 명령에 의해 소비되는 사이클 수를 줄이도록 노력했지만

현대에는 다수 CPU 코어를 이용하도록(병렬성을 이용하도록) 소프트웨어가 작성되어야 한다.

현대의 CPU는 메모리 접근 비용이 CPU 연산 비용보다 상대적으로 비싸졌기 때문에 프로그래머는 CPU에 더 많은 일을 하도록 설계한다.

 

3.4.2 메모리 캐시

캐시는 메인 RAM보다 훨씬 더 빠르게 CPU에서 읽고 쓸 수 있다.

3.4.2.1 캐시 라인

메인 RAM이 256MiB, 캐시라인이 각각 128바이트고 캐시의 크기가 32 KiB인 경우 (256개의 캐시 라인)

캐시는 캐시 라인 크기의 배수에 정렬되는 메모리 주소만 처리할 수 있다.

직접 사상 캐시의 경우

메인 RAM의 어떤 주소에 들어있는 데이터가 캐시에 들어 있는 경우 (메인 RAM의 주소 % 캐시 크기(32KiB)) 값을 캐시 주소로 사용하여 캐시에 접근하면 데이터를 얻을 수 있다.

캐시는 캐시 라인 크기의 배수에 정렬되는 메모리 주소만 처리할 수 있다.

 

TLB를 모두 탐색했을 때 일치하는 블록 인덱스(위의 그림에서 0 ~ 8191)가 있다면 캐시 주소로 캐시에 접근해 데이터를 얻고 없으면 page table을 통해 ram에 접근을 한다.

 

TLB는 context switching때 초기화된다.

 

3.4.2.2 명령어 캐시와 자료 캐시

명령어 캐시 (I-캐시) : 수행되기 전에 실행 기계 코드를 미리 적재하는 것

자료 캐시 (D-캐시): 메인 RAM에서 자료를 읽거나 쓰는 작업의 속도를 높이기 위해 자료를 적재하는 것

보편적으로 L1 캐시에서는 두 종류의 캐시를 따로 존재한다. 

 

3.4.2.3 집합 연합과 대체 정책

각각의 메인 메모리 주소가 두 개 이상의 캐시 라인으로 사상될 수 있다면 더 좋은 성능을 낼 수 있다. (N way 집합 연합 캐시를 사용하는 경우)

 

위 그림(직접 사상 캐시 : 1 way set associateive 캐시)에서는 8192개의 개별 캐시 라인 크기(128byte) 블록이 하나의 캐시 라인으로 사상된다.

 

캐시 미스가 발생하면 여러 개의 캐시 경로 중 어떤 것을 내쫓을 것인지 정하는 방법을 대체 정책이라고 한다. 보통 가장 오래된 자료를 내쫓는다.

 

3.4.2.4 쓰기 정책

1. 직접 쓰기 캐시 : 캐시에 쓰면 RAM에 즉시 적용됨

2. 뒤에 쓰기 캐시 : 자료가 먼저 캐시에 기록된다. 캐시 미스로 인해 새로운 캐시 라인을 읽어들이기 위해 내쫓기는 경우, 프로그램이 명시적으로 캐시 비움을 요청하는 경우 RAM에 캐시 라인이 전달된다. (= RAM에 캐시 라인을 복사한다.)

 

3.4.2.5 다층 캐시

캐시의 크기가 커지면 적중률이 높아져 프로그램이 수행을 더 잘한다. 하지만 캐시의 크기가 커지면 CPU에 가깝게 배치될 수 없어서 더 작은 캐시보다 느려진다.

 

대부분의 게임 콘솔은 두 개 층의 캐시를 채택한다. 레벨 1 캐시에서 자료 찾기를 시도하고 없으면 지연 시간이 더 긴 레벨 2 캐시에서 시도한다. 자료가 L2 캐시에도 없다면 RAM의 접근 비용을 지불해야 한다. (지연 시간이 매우 크다..)

 

3.4.2.6 캐시 일관성: MESI와 MOESI

다수의 CPU 코어가 한 개의 메인 메모리 공간을 공유할 때 문제가 생길 수 있다.

각각의 코어가 자신만의 L1 캐시를 갖는 것은 보통이지만, 다수의 코어가 L2 캐시와 메인 RAM은 공유하게 되기 때문이다.시스템을 캐시 일관성을 유지하는 것이 중요하다. 캐시들에 있는 자료가 서로 일치하고 메인 RAM에 있는 자료와도 일치되도록 유지해야 한다.

보통 사용되는 캐시 일관성 프로토콜은 MESI와 MOESI가 있다.

 

3.4.2.7 캐시 미스 피하기

캐시 미스를 완전히 피하는 것은 불가능하지만 프로그래머는 캐시 미스가 최대한 적게 발생하도록 노력할 수 있다.

 

 D-캐시 미스를 피하는 좋은 방법 : 자료를 가능한 한 제일 작은 연속된 블록으로 조직하고 순차적으로 접근하는 것이다.

예를 들어 객체의 리스트를 순회해야 한다면, 객체는 포인터로 만든 연결리스트가 아닌 배열로 구현(연속된 블록)하고, 객체의 크기는 최대한 줄이고(한정된 캐시의 크기에 최대한 많은 수의 객체를 넣어야 하므로), 배열의 0번 인덱스부터 차례대로 접근(캐시를 알차게 쓸 수 있다) 하면 된다.

 

I-캐시 미스를 피하는 좋은 방법 : 코드 크기 면에서 성능에 가장 영향을 미치는 반복문을 가능한 작게 만들고, 제일 안쪽의 반복문에서 함수 호출을 피하는 것이다. 이것은 반복문 전체 몸체가 반복문이 수행되는 동안 내내 I-캐시 안에 머물도록 해준다.

반복문이 함수를 호출할 필요가 있을 때는, 불려지는 함수의 코드가 메모리 상에서 반복문의 몸체를 포함하는 코드 가까이에 위치하도록 하는 것이 좋다. 컴파일러와 링커가 메모리상에서 코드 배치를 어떻게 할지 결정하기 때문에 사용자는 I-캐시 미스에 대해서 다룰 권한이 없다고 생각할 수 있지만 그렇지 않다.

 

대부분의 C/C++ 링커는 다음과 규칙을 따른다.

1. 단일 함수를 위한 기계어 코드는 거의 항상 메모리상에 연속적이다.

2. 함수는 소스 코드에 나타나는 순서대로 메모리에 배치된다.

 

I-캐시 미스를 피하기 위한 규칙

1. 기계 언어 명령어 수의 관점에서 성능에 영향을 미치는 코드는 가능한 한 작게 만들어라

2. 성능에 민감한 코드 안에서는 함수 호출을 피하라

3. 함수를 호출해야 한다면, 부르는 함수에 최대한 가까이 배치시켜라 (부르는 함수의 앞이나 뒤에 배치시키고, 다른 번역 단위에 놓지 마라)

4. 인라인 함수를 분별 있게 사용하라 : 작은 함수를 인라인으로 바꾸는 것은 성능상의 장점이 될 수도 있지만, 너무 많은 인라인화는 코드 크기를 부풀린다. 불필요하게 커진 코드 부분이 성능에 치명적인 부분 (예를 들어 반복문 내부) 이라면 캐시안에 전부 들어가지 못하게 될 수 있고, 반복문의 반복 한번에 캐시 미스가 두 번 생길 수 있다.

이런 경우에는 코드의 크기를 줄이도록 알고리즘과 구현을 개선하는 방법을 생각해야 한다.

 

3.4.3 명령문 파이프라인과 슈퍼스칼라 CPU (CPU 자체 안에서의 병렬 처리를 향상시킬 수 있는 두가지 아키텍처 구조)

cpu 명령 파이프라이닝 

ㄴ 파이프라인의 지연시간은 하나의 명령어를 완전히 처리하는데 소요되는 시간이다. 파이프라인의 각 단계중 가장 오래걸리는 지연 시간을 기준으로 결정된다.

 

슈퍼스칼라 프로세서는 파이프라인 단계들의 일부분이나 전체에 대한 다중 회로 복사본을 포함한다. 이는 여러 개의 명령어 스트림을 병렬로 처리할 수 있다.

예를 들어 CPU가 두 개의 정수 산술/논리 단위(ALU)를 가지고 있다면, 두 개의 정수 명령어가 동시에 처리될 수 있다.

 

3.4.3.1 자료 의존과 칸막이

파이프라인식 CPU는 새로운 명령어를 모든 클락 사이클마다 실행시켜서 가지고 있는 모든 단계(fetch, decode, execute, memory, register write-back)를 활성화 시키려 한다. 어떤 명령어의 결과가 다른 명령어를 실행시키기 위해서 필요하다면 

자료 의존 문제가 발생하여 파이프라인에 칸막이(stall)을 만든다.

 

move 5, r3

mul r0, 10, r1

add r1, 7, r2

 

와 같은 어셈블리어 명령문을 실행하게 되면 add는 mul의 결과 (r1)을 사용해야 하기 때문에 mul 명령어의 register write-back 단계가 끝날 때까지 기다려야 한다.

최적화 컴파일러에서는 stall을 피하기 위해 add r1, 7, r2 뒤에 있는 의존적이지 않은 명령들을 mul과 add 사이로 옮긴다.

 

3.4.3.2 분기 예측

if 문을 만나게 되면 stall이 발생한다. 기다리는 대신에 분기를 하나 골라서 계속 실행하고 분기문의 조건 계산이 끝난 후 추측이 맞다면 그대로 진행하고 추측이 틀렸다면 파이프라인은 비워지고 올바른 분기의 첫 번째 명령어가 재시작되어야 한다.

어떤 분기를 선택할지 예측은 하드웨어가 한다. 분기 예측 하드웨어 성능이 좋아야 한다.

'읽은 책 > 게임 엔진 아키텍처' 카테고리의 다른 글

6. 리소스 시스템과 파일 시스템  (0) 2022.08.08
5. 엔진 지원 시스템  (0) 2022.08.06
4. 게임에 사용되는 3D 수학  (0) 2022.08.04
2. 도구  (0) 2022.07.31
1. 소개  (0) 2022.07.30

+ Recent posts