15.1 게임플레이 기반 시스템의 컴포넌트

대부분의 게임 엔진은 게임의 고유한 규칙과 목표, 그리고 동적 월드 구성 요소들을 만드는 기반으로 쓰이는 여러 런타임 소프트웨어 컴포넌트를 제공한다. 이런 컴포넌트를 게임플레이 기반 시스템이라고 부르기로 하자.

 

게임 엔진마다 게임 플레이 소프트웨어 디자인에 대한 접근이 조금씩 다르다.

 

대부분의 엔진이 지원하는 주요 하위 시스템

1. 런타임 게임 객체 모델

ㄴ 게임디자이너가 월드에디터를 통해 인지하는 가상의 게임 객체 모델을 실제로 구현하는 부분

 

2. 레벨 관리 및 스트리밍

ㄴ 게임 플레이가 벌어지는 가상 월드의 콘텐츠를 불러오고 내리는 시스템,

ㄴ 레벨 데이터를 메모리에 스트리밍하면 플레이어는 광활하고 연속된 월드에 있는 것처럼 인지하게 된다.

 

3. 실시간 객체 모델 업데이트

ㄴ 게임 객체들이 스스로 동작하게 하려면 모든 객체를 주기적으로 업데이트해줘야 한다.

 

4. 메세지와 이벤트 처리

ㄴ 대부분의 게임 객체들은 서로 통신해야 한다. 이런 통신은 대개 추상적 메세지 시스템을 통해 이뤄진다.

 

5. 스크립트

ㄴ 하이레벨 게임 로직을 C나 C++ 등의 프로그래밍 언어로 짜면 매번 컴파일을 해줘야 한다. (느리다)

생산성을 향상시키면서 빠른 반복 생산을 가능하게 하기 위해 게임 엔진에 스크립트 언어를 통합하는 경우가 많다.

파이썬이나 루아같이 텍스트 기반인 언어도 있고, 언리얼 엔진의 Kismet처럼 그래픽 언어인 경우도 있다.

 

6. 목적 및 게임 흐름

 

엔진마다 부분적으로 지원하는 시스템

1. 동적으로 게임 객체를 생성하고 파괴하기

ㄴ 게임월드의 동적인 구성 요소들을 게임 플레이 도중에 만들었다가 없애는 경우가 자주 있다.

 

2. 로우레벨 엔진 시스템과의 연동

ㄴ 모든 게임 객체는 적어도 한두 가지 이상의 하위 엔진 시스템과 연관이 있다. 게임 객체들이 각자 필요한 하위 엔진 시스템에 접근할 수 있게 해주는 것이 게임플레이 기반 시스템의 주요한 역할 중 하나다.

 

3. 새로운 게임 객체 타입을 정의할 수 있는 기능

ㄴ 새로운 객체 타입을 쉽게 추가하고 월드에디터에서 사용할 수 있게 유연한 게임 객체 모델을 갖는것이 중요하다.

ㄴ 아주 이상적인 상황이라면 데이터 주도 방식(스크립트)만으로 새로운 객체 타입을 정의할 수도 있지만 대부분의 엔진에서는 프로그래머의 손을 거친다.

 

4. 고유 객체 식별자(id)

ㄴ 게임 월드 안에는 다양한 종류의 게임 객체들이 수백 혹은 수천 개 존재하는 경우가 흔하기 때문에 런타임에 각 객체들을 구분하고 찾을 수 있는 기능이 중요하다.

ㄴ정수 식별자나 문자열 식별자를 사용한다.

 

5. 게임 객체 질의

ㄴ 게임 월드 내에서 게임 객체를 찾을 수 있는 방법이 반드시 있어야 한다.

ㄴ 예를 들면 플레이어 캐릭터 주위 20미터 안에 있는 모든 적을 찾는 것

 

6. 게임 객체에 대한 참조

ㄴ 객체를 찾았으면 객체를 참조하는 값을 담을 수 있어야한다.

ㄴ C++ 클래스 인스턴스에 대한 포인터나 스마트 포인터일 수도 있다.

 

7. 유한 상태 기계에 대한 지원

ㄴ 게임 객체 타입 중 유한 상태 기계로 모델링하면 가장 잘 들어맞는 것들이 종종 있다. 게임 객체가 한 번에 한 가지 상태를 가질 수 있게 지원하는 엔진도 있다.

 

8. 네트워크 레플리케이션

ㄴ멀티플레이어 게임에서의 게임 객체의 상태는 다른 모든 기계에 복제돼서 모든 플레이어들이 그 객체를 일관된 모습을 볼 수 있어야 한다.

 

9. 게임을 저장하고 불러오는 기능/객체의 영속성

ㄴ 게임 엔진이 월드 내의 게임 객체들의 현재 상태를 디스크에 저장하고 나중에 불러올 수 있도록 하는 기능

ㄴ RTTI, 리플렉션, 추상 생성같은 프로그래밍 언어의 기능을 필요로 한다.

RTTI, 리플렉션은 런타임에 객체의 타입 뿐만 아니라 그 객체의 클래스가 어떤 속성과 메서드를 지원하는지를 알 수 있게 해주고, 추상 생성은 클래스 이름을 하드코딩하지 않고도 그 클래스의 인스턴스를 사용할 수 있게 해준다.

 

15.2 런타임 객체 모델 구조

월드 에디터에서 디자이너들에게 제공되는 툴 측면 추상적 객체 모델을 게임에서 구현한 것을 런타임 객체 모델이라 할 수 있다.

런타임 객체 모델의 구현은 툴 측면의 추상적 객체 모델과 비슷할 수도 있고, 완전히 다를 수도 있다. 다양한 방식이 쓰이지만 대부분의 게임 엔진들은 다음의 두 가지 기본적인 구조 중 한 가지를 따른다.

1. 객체 중심적 구조

ㄴ 툴 측면의 게임 객체 하나는 런타임에서 클래스 인스턴스 한 개로 표현되거나, 아니면 서로 연관 있는 적은 숫자의 인스턴스 집합으로 표현된다.

ㄴ 객체는 자신의 속성과 행동을 가지며, 이것들은 객체의 클래스 내에 캡슐화 되어 있다.

 

2. 속성 중심적 구조

ㄴ 툴 측면의 게임 객체들은 각각 고유 id로만 표현된다.

ㄴ 각 게임 객체의 속성들은 속성 타입별로 데이터 테이블에 나뉘어 담겨 있으며, 접근할 때는 객체의 id를 이용한다.

 

15.2.1 객체 중심 구조

15.2.1.2 거대 단일 클래스 계층

하나의 공통 베이스 클래스를 거의 모든 객체들이 상속하는 경우

언리얼 엔진의 게임 객체 모델

 

15.2.1.3 깊고 넓은 계층 구조의 문제점

ㄴ 클래스들을 이해하기 힘들고 유지 및 수정이 어려움

 

ㄴ 여러 계열의 분류 구조를 구현할 수 없는 문제

예를 들어 생물체를 유전적인 형질을 기준으로 분류하면서 동시에 색깔에 따라 분류할 수 없다.(다중 상속의 문제)

다중 상속(is-a)보다는 합성(has-a)이나 조합을 사용하는 편이 낫다.

 

ㄴ 버블업 효과 

언리얼 엔진의 Actor 클래스는 렌더링, 물리, 월드와의 상호작용, 오디오 효과, 멀티플레이어용 네트워크 레플리케이션, 객체 생성 및 파괴, 액터 반복, 메세지 브로드캐스팅 등을 위한 데이터 멤버와 코드를 모두 가지고 있다. 이렇게 최상위 루트 클래스에 여러 기능이 버블업 하게 내버려 두면 여러 엔진 하위 시스템의 역할을 캡슐화하기 어려워진다.

 

15.2.1.4 합성을 통해 계층 구조를 단순하게 유지

합성 관계에서는 클래스 A가 클래스 B의 인스턴스를 직접 포함하거나 아니면 B의 인스턴스에 대한 포인터나 참조를 포함한다. A의 인스턴스가 생성될 때와 파괴될 때 B도 같이 생성되고 파괴되는 경우를 합성(composition) 이라고 한다. 두 클래스 사이에 포인터나 참조를 사용해 한 클래스가 다른 클래스의 생성과 파괴를 직접 관리하지 않게 만들 수도 있다. 이 경우는 조합(aggregation) 이라고 부른다.

 

컴포넌트 디자인을 활용하면 만들고자 하는 타입의 게임 객체가 꼭 필요로 하는 기능들만 넣게 선택할 수 있다.

https://lemonyun.tistory.com/78

 

14. 디커플링 패턴 - 컴포넌트

한 개체가 여러 분야를 서로 커플링 없이 다룰 수 있게 한다. 유니티 게임 오브젝트에 여러 컴포넌트를 붙였던 경험을 떠올려 쉽게 이해할 수 있었다. 멀티스레드 환경에서는 게임 코드를 분야

lemonyun.tistory.com

 

컴포넌트 생성과 소유권

ㄴ 단순한 방법 : GameObject 클래스가 가능한 모든 컴포넌트의 포인터를 갖고, 게임 객체들은 GameObject를 상속받아 정의한다. GameObject의 생성자에서는 모든 컴포넌트에 대한 포인터를 NULL로 초기화한다(하드 코딩). 상속받은 클래스의 생성자는 각자 필요에 따라 컴포넌트를 생성하면 된다.

ㄴ GameObject의 소멸자에서 모든 컴포넌트에 대한 포인터를 지워주는 식으로 구현하면 편리하다.

 

15.2.1.5 제네릭 컴포넌트

ㄴ 유연한 (구현하기에는 까다로운 방법) : 루트 클래스에 제네릭연결 리스트를 두어 컴포넌트들을 관리하는 방식

ㄴ 객체에 새로운 타입의 컴포넌트를 추가할 때 게임 객체 클래스를 수정하지 않을 수 있다.

ㄴ 연결 리스트를 순회하며 각 컴포넌트에 대해 타입을 질의한다거나 차례로 이벤트를 넘겨 처리할 기회를 주는 등 다형적인 연산을 할 수 있다.

 

15.2.1.6 순수 컴포넌트 모델 (속성 중심 디자인)

ㄴ GameObject '허브' 클래스를 둘 필요 없이 각 컴포넌트마다 해당 게임 객체의 고유 id를 복사해 넣는 방법 (컴포넌트 = 속성 중심 구조)

순수 컴포넌트 모델에서는 팩토리 패턴을 사용하면 된다. 가상 함수가 있는 팩토리 클래스를 게임 객체 타입마다 한 개씩 만들고, 가상 함수를 오버라이드해 객체마다 원하는 컴포넌트를 생성하게 한다.

 

ㄴ 순수 컴포넌트 디자인의 문제는 컴포넌트 간 통신을 중계해줄 GameObject 클래스가 없다는 것이다. 컴포넌트 간 통신이나 다른 게임 객체로 메세지를 보내는 일도 까다롭다.

 

15.2.2 속성 중심적 구조

게임 객체가 가질 수 있는 모든 속성의 집합을 정의하고, 속성마다 테이블을 만든다. 테이블에서 속성과 게임객체 ID가 매핑된다. (관계형 데이터베이스에 가깝다)

객체는 속성에 의해서만 정의되는 것이 아니라 행동에 의해서도 정의되기 때문에 행동을 구현하는 방법은 여러가지이며, 엔진마다 다르게 구현한다.

 

15.2.2.1 속성 클래스를 통해 행동 구현

속성 타입은 속성 클래스로 구현할 수 있다.

각 속성 클래스는 하드코딩된 메서드(멤버 함수)를 통해 행동을 제공한다.

어떤 게임 객체의 전체적인 행동은 그 객체의 모든 속성들의 행동이 합쳐져 결정된다.

 

15.2.2.2 스크립트를 통해 행동 구현

속성 값들은 데이터베이스형 테이블에 담아 두고 게임 객체의 행동을 스크립트 코드를 이용해 구현하는 방법이 있다.

게임 객체는 ScriptId와 같은 특수한 속성을 가질 수 있고, 이 속성이 있을 경우 이것은 해당 객체의 행동을 처리할 스크립트 코드 블록을 나타낸다.게임 월드의 이벤트에 게임 객체가 반응하게 만들 때도 스크립트 코드를 사용한다.

 

15.2.2.4 속성 중심 디자인의 장단점

장점

ㄴ 실제로 사용되고 있는 속성 데이터만 저장하면 되기 때문에 메모리를 적게 쓰는 경향이 있다.

ㄴ 게임 코드를 컴파일할 필요 없이 새 속성을 쉽게 정의할 수 있다.

ㄴ 같은 타입의 데이터가 메모리에 연속적으로 저장되기 때문에 캐시 효율이 좋을 수 있다. (배열의 구조체 방식을 사용한다면)

 

단점

ㄴ 속성들 간에 어떤 관계를 강제하기가 어려워진다.

ㄴ 게임 객체가 가지는 속성을 한눈에 볼 수 없기 때문에 디버깅이 어렵다.

 

15.3 월드 덩어리 데이터 형식

월드 청크에는 정적 월드 요소와 동적 월드 요소가 같이 들어간다.

 

월드 덩어리 데이터 파일에는 게임 객체마다 다음과 같은 것들이 저장된다.

객체 속성들의 초기 값

ㄴ 월드 청크는 모든 게임 객체가 처음 게임 월드에 생성될 때의 상태를 정의한다. 객체의 상태 데이터는 여러 형식으로 저장할 수 있다.

 

객체 타입에 대한 지정

ㄴ 객체 중심 엔진에서는 문자열, 해시 문자열 ID, 혹은 기타 고유한 타입 id 등을 이용한다. 속성 중심 디자인에서는 타입을 그대로 저장하기도 하고, 아니면 객체를 이루는 속성들에 따라 암묵적으로 정의될 수도 있다.

 

15.3.1 바이너리 객체 이미지

게임 객체들을 디스크 파일로 저장하는 방법 중에는 런타임에 메모리에 존재하는 모든 객체의 바이너리 이미지를 파일로 저장하는 방식이 있다. 

하지만 C++ 클래스 인스턴스를 바이너리 이미지로 저장하는 것은 문제가 있다. 

ㄴ 포인터와 가상 테이블을 예외적으로 처리해야 한다.

ㄴ 클래스 인스턴스 내 데이터의 endian을 처리해야 한다.

이런 이유로 객체 이미지를 저장하는 형식은 게임 객체 데이터를 저장하는데 그다지 좋은 선택이 아니다.

 

15.3.2 게임 객체 정보의 직렬화

직렬화를 사용하면 바이너리 객체 이미지를 저장하는 것보다 여러 플랫폼에 옮기기 쉽고 구현하기가 간편하다.


C#이나 자바는 모두 객체 인스턴스를 XML 텍스트 형식으로 직렬화하는 표준 방식을 지원한다. 하지만 C++은 표준화된 직렬화 기능을 지원하지는 않는다.

 

XML은 파싱이 느리다는 단점이 있어서 월드 덩어리를 읽어 들이는 시간이 느려질 수 있다. 그래서 더 빠르게 파싱할 수 있고 크기도 더 작은 자체 구현 바이너리 형식을 사용하는 게임엔진도 있다.

 

객체를 디스크에 저장하고 불러오는 직렬화 기법을 구현하는 데는 다음과 같은 기본적인 두 가지 방식 중에서 하나가 쓰인다.

 

1. 부모 클래스에 SerializeOut()과 SerializeIn() 등의 이름이 붙은 가상 함수 두 개를 넣고 상속받는 클래스는 이 함수들을 구현해 자신들의 속성을 직렬화하게 한다.

 

2. C++ 클래스에 리플렉션 시스템을 구현한다. 그런 후 리플렉션 정보가 있는 모든 C++ 객체를 자동으로 직렬화하는 제네릭 시스템을 만든다. (범용적임)

 

속성 정보에 더해 직렬화 데이터 스트림은 반드시 객체의 클래스나 타입을 나타내는 이름이나 고유 id를 포함한다. 이런 클래스 id는 디스크에서 메모리로 객체를 직렬화할 때 올바른 클래스 객체를 생성하는데 쓰인다.

 

하지만 C++에서는 이름을 나타내는 문자열이나 id만 가지고 클래스 인스턴스를 생성할 수 없다. C++에서는 이런 문제를 우회하기 위해 클래스 팩토리를 사용한다. 클래스 팩토리를 구현하는 방법은 여러 가지가 있지만, 가장 단순한 방식은 데이터 테이블을 만들어 클래스이름/id를 가지고 그 클래스 인스턴스를 생성하는 하드 코딩된 함수나 펑터로 연결해주는 것이다. id에 해당하는 함수나 펑터를 테이블에서 찾아 이것들을 호출해 클래스 인스턴스를 생성한다.

 

15.3.3 스포너와 타입 스키마

바이너리 객체 이미지와 직렬화는 게임 객체 타입의 런타임 구현에 따라 좌우되기 때문에 월드 에디터가 게임 엔진의 런타임 구현에 대해 상세히 알고 있어야 한다는 단점이 있다.

 

게임 월드 에디터와 런타임 엔진 코드 간의 결합을 끊으려면 게임 객체에 대한 정보를 구현 중립적인 방식으로 추상화하면 된다. 월드 청크 데이터 파일에 있는 모든 객체마다 작은 데이터 블록(스포너)를 저장한다. 스포너는 가볍고 데이터만 있는 게임 객체 표현으로, 런타임에 해당 게임 객체의 인스턴스를 생성하고 초기화하는 데 사용된다.

 

스포너에는 툴에서 쓰이는 게임 객체 타입에 대한 id, 속성들의 초기값을 저장한 테이블, 월드 행렬 등이 저장된다.

게임 객체를 생성(스폰)할 때 스포너의 타입을 보고 올바른 클래스 인스턴스가 생성된다. 

 

스포너는 로딩되자마자 해당하는 게임 객체를 생성하게 설정할 수도 있고 게임 실행 중 생성 요청이 올 때까지 기다릴 수도 있다.

 

스포너는 게임 월드 내의 중요 지점이나 좌표축들을 정의하는데 사용될 수도 있다.

 

15.3.3.1 객체 타입 스키마 (= 게임 객체 스키마)

스포너 방식의 디자인을 채용한 게임 월드 에디터에서는 게임 객체 타입을 나타낼 때 데이터 기반 방식의 스키마를 사용할 수 있는데, 스키마는 어떤 타입의 객체를 만들고 편집할 때 알아야 할 속성들을 정의한 것이다.

 

객체 타입 스키마를 클래스처럼 상속받는 형태로 지원하는 엔진도 있다. 객체 타입을 비롯해 런타임에 다른 게임 객체와 구분할 수 있는 고유 id는 모든 게임 객체에 있어야 하는데, 이것들은 탑 레벨의 스키마에 정의해서 다른 스키마들이 상속 받을 수 있게 한다.

 

15.3.3.2 디폴트 속성 값

게임 디자이너가 어떤 게임 객체 타입의 인스턴스를 게임 월드에 집어넣고자 할 때 수많은 속성의 값을 정해줘야 하기 때문에 스키마들의 속성들 중에 디폴트 값을 정의할 수 있는 것들이 많다면 큰 도움이 된다.

 

15.4 게임 월드의 로딩과 스트리밍

오프라인 월드 에디터와 런타임 게임 객체 모델을 연결시키려면 월드 덩어리를 메모리에 불러오고, 필요 없어지면 다시 내릴 방법이 있어야 한다.

 

게임 월드 로딩 시스템은 두 가지 중요한 역할을 맡는다.

1. 게임 월드 청크와 기타 필요한 자원들을 디스크에서 메모리로 불러 오는 파일 I/O 관리 역할

2. 자원들에 필요한 메모리를 할당하고 해제 하는 것을 관리하는 역할

 

15.4.1 단순한 레벨 로딩

가장 직관적인 게임 로딩 방식은 한 번에 오직 하나의 게임 청크만 로딩되게 하는 것이다.

ㄴ 레벨이 전환될 때 플레이어는 로딩 스크린을 보고 있어야 한다.

ㄴ 이 방식의 월드 로딩 디자인에는 스택 기반 할당자가 어울린다.

 

게임이 맨 처음 시작할 때 게임의 모든 레벨에 걸쳐 공통적으로 사용되는 자원들이 스택의 맨 아래에 불러온다. 이것을 LSR(load-andstay-resident) 데이터라고 부르며 스택에서 맨 아래 공간에 들어온다. LSR이 채워지는 공간 위의 메모리 공간에 게임 월드를 위한 데이터가 들어왔다 나갔다 한다.

 

15.4.2 심리스(Seamless : 끊김없는, 자연스러운) 로딩에 다가가기 : 에어 락

광활하고 연속된 seamless 월드를 구현하기 위한 방법이다.

 

게임 월드 자원을 위해 마련해 놓은 메모리를 똑같은 두 개의 블록으로 나누면 된다. 한쪽 블록에 레벨 A를 불러온 후 플레이어가 레벨 A에서 게임을 시작하게 하고, 그 후 스트리밍 파일 I/O 라이브러리(별도의 스레드에서 구현)를 사용해 다른 블록에 레벨 B를 불러오면 된다. 이 방식의 문제점은 월드에 할당할 수 있는 메모리가 기존의 절반으로 줄어든다는 점이다.

 

비슷하게 게임 월드 메모리를 두 개의 크기가 다른 블록으로 나누는 방법이 있다.

큰 블록에는 온전한 게임 월드 청크가 들어가고, 작은 블록은 아주 작은 청크가 겨우 들어갈 만큼의 크기를 가진다.(에어 락)

 

플레이어가 온전한 청크에서 에어 락에 들어갈 때, 플레이어는 문이나 다른 장애물 때문에 온전한 월드 지역을 보거나 다시 돌아가지 못한다. 이 와중에 온전한 청크를 메모리에서 내리고 새로운 온전한 크기의 월드 청크를 로딩한다.

 

결론적으로 에어락은 비동기 I/O를 이용해 플레이어가 로딩 스크린을 보지 않고도(게임 플레이가 끊기지 않고) 다음 단계의 월드 청크로 진행할 수 있게 해준다.

 

15.4.3 게임 월드 스트리밍

어떤 게임 디자인은 플레이어가 광활하고 이어져 있는 심리스 월드에서 플레이하고 있다는 느낌을 줘야 한다.

플레이어가 좁은 에어락 지역에 반복적으로 들어갈 필요가 없게 만들도록 해야 한다. 이를 위해 스트리밍이라는 기법을 사용한다. 

 

여러개의 월드 청크를 메모리에 동시에 올려 놓고 캐릭터가 월드 청크에 진입하게 되거나 빠져나올 때 할당, 해제하는 방식으로 스트리밍을 구현할 수도 있는데, 크기가 큰 월드 청크를 스트리밍하는 대신에 전경(foreground), 메시, 텍스처, 애니메이션 등 모든 게임 자원을 같은 크기의 블록이 되게 쪼갤 수도 있다. 그런 후에 풀 기반 메모리 할당 시스템을 사용하면 메모리 단편화 걱정 없이 필요할 때 자원 데이터를 로딩하고 내릴 수 있다.

 

15.4.3.1 불러올 자원 결정

어떤 월드가 메모리에서 내려가면 플레이어는 절대 이 월드를 볼 수 없어야 하고, 

어떤 월드가 메모리에 올라가 플레이어가 이 월드를 보기 전까지 충분한 시간을 두어야 한다.

 

15.4.4 객체 생성을 위한 메모리 관리

대부분의 게임 엔진에는 게임 객체들을 이루는 클래스들을 인스턴싱하고 필요 없어지면 파괴하는 일을 담당하는 게임 객체 생성(스포닝) 시스템이 있다.

동적 할당은 느린 과정이기 때문에 효율적으로 만들어야 한다.(메모리 단편화를 피해야 한다.)

 

15.4.4.1 객체 생성을 위한 오프라인 메모리 할당 사용

ㄴ 게임플레이 도중에 동적인 메모리 할당을 금지하는 방법

ㄴ 게임 객체들이 사용할 메모리를 월드 에디터가 오프라인에 할당할 수 있고, 월드 청크 데이터 안에 포함시킬 수 있다.

ㄴ 한 청크 안의 모든 게임 객체들이 사용하는 메모리 양이 미리 계산될 수 있기 때문에 메모리 단편화를 방지할 수 있다.

ㄴ 동적 객체 생성을 흉내내기 위해 월드에 필요한 수 만큼의 객체를 할당한 후 객체의 상태를 잠들고 깨우는 방식을 사용한다.

 

15.4.4.2 객체 생성을 위한 동적인 메모리 관리

타입이 다른 게임 객체들은 차지하는 메모리 크기가 다르기 때문에 흔히 많이 쓰이는 단편화 없는 풀 할당자를 사용할 수 없게 된다.

게임 객체들은 보통 생성된 순서와 상관 없이 파괴되기 때문에 스택 할당자도 사용할 수 없다.

남은 것은 힙 할당자 뿐인데 이는 단편화에 취약하다. 그래도 대안이 몇가지 있다.

 

1. 객체 타입마다 하나의 메모리 풀 사용

ㄴ 여러 풀을 관리해야 한다.

ㄴ 각 객체 타입별로 얼마나 많은 수를 사용할지 경험적으로 유추해야 한다.

 

2. 작은 메모리 할당자

ㄴ 관리해야 할 풀의 종류는 줄어들지만 풀마다 잠재적으로 낭비하는 메모리가 있을 수 있다.

ㄴ 할당 단위 크기가 두배로 늘어나는 메모리 풀들을 만들어 사용할 수 있다.

ㄴ 게임 객체를 위해 메모리를 할당할 때, 할당 단위가 객체의 크기와 같거나 이보다 큰 풀들 중에서 가장 작은 것을 찾는다.

 

3. 메모리 재배치

ㄴ 5.2.2.2 내용

 

15.4.5 게임 저장

월드 로딩 시스템과 저장 시스템은 비슷하지만 요구 조건이 다르기 때문에 따로 구현된다.

월드 청크는 월드 안에 있는 모든 동적 객체의 초기 상태를 담고 있고, 이에 더해 모든 정적 월드 요소에 대한 완전한 정보도 담는다.

배경 메시와 충돌 데이터 등의 정적 정보는 디스크 공간을 많이 차지하는 경우가 보통이기 때문에 월드 청크는 여러 파일로 구성되는 경우도 있고 연관된 데이터의 크기도 대개 크다.

 

게임 저장파일도 게임 객체의 현재 상태를 저장해야 하지만, 월드 청크에서 알 수 있는 정보들은 중복 저장할 필요가 없다.

그래서 게임 저장 파일은 월드 청크 파일보다는 훨씬 작으며, 데이터 압축과 생략에 더 중점을 둘 수 있다.

 

15.4.5.1 체크 포인트

체크 포인트라고 불리는 특정한 지점에서만 저장할 수 있게 하는 방식

체크 포인트 주변의 월드 청크에 게임의 상태가 저장된다.

어느 누가 플레이하든 데이터가 거의 비슷하기 때문에 따로 저장할 데이터가 적다. = 저장 파일의 크기가 작다.

 

15.4.5.2 자유 저장

게임 플레이 도중에 언제든 게임 상태를 저장할 수 있는 기능

게임 저장 파일의 크기가 커진다.

게임 저장 파일의 내용이 월드의 정적인 요소들만 빼고 월드 청크의 정보와 비슷하다.

 

15.5 객체 참조와 월드 질의

게임 객체의 id는 게임 내의 객체들을 구분하거나 런타임 검색, 객체 간 통신에서 수신자 명시 등에 쓰인다. 월드 에디터 툴에서도 검색과 구분을 위해 흔히 쓰인다.

 

런타임에는 게임 객체들을 검색할 수 있는 기능이 반드시 있어야 한다. 고유 id로 객체를 찾는 경우도 있고, 객체 타입이나 아니면 다른 조건으로 검색하는 경우도 있다.

 

질의를 통해 찾은 객체는 포인터, 핸들, 스마트 포인터를 이용해 참조를 구현할 수 있다.

 

15.5.1 포인터

객체 참조를 구현하는데 가장 빠르고 효율적이고 다루기도 쉽지만, 프로그래머에게 포인터 관리에 대한 부담이 클 수 있다. (실수하기 쉽다)

 

15.5.2 스마트 포인터

평상시에는 여느 포인터처럼 동작하지만 C/C++ 네이티브 포인터의 문제들은 해결한 작은 객체를 스마트 포인터라고 부른다.

포인터 역참조 기능을 지원하도록 *와 -> 연산자를 오버로딩해 원래 메모리 주소를 리턴한다.

 

ㄴ 3.1.3.6 내용

 

15.5.3 핸들

핸들 테이블에는 객체에 대한 포인터가 담겨 있다. 핸들은 이 테이블에 대한 인덱스일 뿐이다.

핸들의 문제점은 낡은 객체를 참조할 가능성이 있다는 점이다.

이 문제를 해결하려면 모든 핸들에 객체의 고유 id를 포함하게 만들면 된다.

 

// 핸들을 사용하여 객체를 참조할 때의 객체 클래스
class GameObject
{
private:
    //..
    GameObjectId m_uniqeud; // 객체의 고유 id
    U32 m_handleIndex; // 핸들 생성을 빠르게 하기 위해
    friend class GameObjectHandle; // id와 index에 접근할 수 있게
    
public:
    GameObject()
    {
        // 고유 id는 월드 에디터에서 올 수도 있고, 
        // 아니면 런타임에 동적으로 할당할 수도 있다.
        
        m_uniqueId = AssignUniqueObjectId();
        
        // 핸들 테이블에서 비어있는 첫 번째 슬롯을 찾아 핸들 인덱스를 얻어온다.
        m_handleIndex = FindFreeSlotInHandleTable();
    
    }
};
    
    // 글로벌 핸들 테이블
static GameObject* g_apGameObject[MAX_GAME_OBJECTS];
    
// 간단한 객체 핸들 클래스
class GameObjectHandle
{
private:
    U32 m_handleIndex; // 핸들 테이블의 인덱스
    GameObjectId m_uniquewId; // 낡은 핸들을 방지하기 위한 고유 id

public:
    explicit GameObjectHandle (GameObject& object):
        m_handleIndex(object.m_handleIndex),
        m_uniqueId(object.m_uniqueId)
        { }
    
    // 핸들을 역참조하는 함수
    GameObject* ToObject() const
    {
        GameObject* pObject = g_apGameObject[m_handleIndex];
        if(pObject != NULL && pObject -> m_uniqueId == m_uniqueId)
        {
            return pObject;
        }
        
        return NULL;
    }
};

 

15.5.4 게임 객체 질의

게임 개발 중에 어떤 타입의 질의가 가장 많이 필요할지를 미리 결정한 다음 그 질의를 수행하는데 가장 효율적인 자료구조를 구현한다.

 

고유 id로 게임 객체 찾기

ㄴ 게임 객체를 가리키는 포인터나 핸들을 해시 테이블에 넣거나 이진 검색 트리에 넣고 고유 id로 찾는다.

 

특정 조건을 충족하는 모든 게임 객체를 순회하기

ㄴ 게임 객체들을 여러 조건에 맞춰 정렬한 후 연결 리스트에 미리 저장해 둔다.

ㄴ 예를 들면 특정 타입의 모든 게임 객체를 리스트로 만들거나 플레이어로부터 특정 범위 안에 있는 모든 객체를 리스트로 만들 수 있다.

 

발사체의 이동 경로에 있거나 어떤 목표 지점까지의 위에 있는 모든 객체를 찾기

ㄴ 보통 충돌 시스템을 활용한다. 충돌 시스템은 레이 캐스트나 형상 캐스트를 통해 충돌하는 물체를 빠르게 찾아낼 수 있다.

 

15.6 실시간 게임 객체 업데이트

대부분의 로우레벨 엔진 하부 시스템(렌더링, 애니메이션, 충돌, 물리, 오디오 등)은 주기적으로 업데이트해야 하는데, 게임 객체도 마찬가지다.  모든 게임 엔진은 메인 게임 루프의 일부분에서 게임 객체 상태를 업데이트 한다고 말할 수 있다. 

 

15.6.1 단순한 접근 방식(하지만 동작하지 않는 방식)

게임 객체들 전체의 상태를 업데이트하는 가장 단순한 방법은 순회하면서 가상 함수, 즉 Update() 같은 함수를 모든 객체마다 차례대로 호출하는 것이다.

게임 객체들은 Update() 함수를 오버라이딩하고 이전 프레임과의 시간차를 인자로 전달 받는다.

 

모든 게임 객체를 아우르는 집합을 어떻게 관리할 것인가?

Update() 함수가 해야 할 일은 어떤 것인가?

 

15.6.1.1 활성 게임 객체의 집합 관리

활성(active) 게임 객체를 관리하는 데는 주로 싱글턴 매니저 클래스를 쓰는 경우가 많으며, 대개 GameWorld나 GameObjectManager 등의 이름을 갖는다.

 

게임 객체들은 게임이 진행되면서 생성되고 파괴되기 때문에 게임 객체의 집합은 보통 동적인 경우가 대부분이다.

대부분의 게임 객체는 게임 객체를 관리하는 데 단순한 연결 리스트보다는 좀 더 복잡한 자료 구조를 사용한다.

 

15.6.2 성능 조건과 일괄 업데이트

대부분의 상용 게임 엔진들은 게임 루프에서 각 엔진 하부 시스템을 직접 혹은 간접적으로 업데이트하며, 게임 객체의 Update() 함수 안에서 하지 않는다.

게임 객체가 어떤 하부 시스템을 이용할 일이 있으면 직접 하부 시스템에 요청해 자신을 위한 상태 정보를 할당하게 만들 수 있다.

 

virtual void Tank::Update(float dt)
{
    // 탱크의 상태를 업데이트 한다.
    MoveTank(dt);
    DeflectTurret(dt);
    FireIfNecessary();
    
    // 여러 엔진 하부 시스템의 속성을 설정하지만 
    // 직접 업데이트 하지는 않는다.
    
    // 조건에 따라 객체에 대한 하부 시스템의 속성을 다르게 설정
    if(justExploded)
    {
        m_pAnimationComponent-> PlayAnimation("explode");
    }
    
    if(isVisible)
    {
        m_pCollisionComponent->Activate();
    }
    else
    {
        m_pCollisionComponent->Deactivate();
    }
}

// 게임 루프
while (true)
{
    PollJoypad();
    float dt = g_gameClock.CalculateDeltaTime();
    
    // 모든 게임 객체의 update() 
    // 여러 엔진 하부 시스템들의 속성들만 설정
    for (each gameObject)
    {
     	gameObject.Update(dt);   
	}
    
    // 하부 시스템들의 일괄 업데이트
    g_animationEngine.Update(dt);
    g_physicsEngine.Simulate(dt);
    g_collisionEngine.DetectAndResolveCollision(dt);
    g_audioEngine.Update(dt);
    g_renderingEngine.RenderFrameAndSwapBuffers();
}

일괄 업데이트가 가져올 수 있는 성능 이득

 

1. 최대의 캐시 일관성

ㄴ 객체들의 데이터를 연속된 RAM 공간에 모을 수 있다.

 

2. 계산 중복 최소화

ㄴ 공통적인 연산은 한 번만 하고 매 객체는 다시 이것을 계산하기보다는 계산된 것을 가져와 활용하면 된다.

 

3. 자원 재할당 감소

ㄴ 엔진 하부 시스템들은 업데이트하는 동안 메모리를 비롯한 자원을 할당하고 관리하는 일을 하는 경우가 많은데, 일괄 업데이트를 사용하면 프레임에서 한 번만 자원을 할당한 후 모든 객체에서 돌아가며 재활용할 수 있다.

 

4. 효율적인 파이프라인화

ㄴ 엔진 하부 시스템들은 게임 월드에 있는 모든 객체마다 근본적으로 동일한 연산을 수행하는 경우가 많다. 일괄 업데이트를 이용하면 새로운 최적화를 가능하게 할 수도 있고 전용 하드웨어 자원을 활용하는 것도 가능하다.

 

동적 강체들이 존재하는 시스템에서 충돌을 해결할 때 객체 간의 교차를 해결하려면 객체들을 그룹으로 처리해야 한다. 일괄 업데이트는 이런 면에서도 필수적이다.

 

15.6.3 객체와 하부 시스템 간 상호 의존

객체간 의존 관계가 있거나 엔진 하부 시스템끼리 의존 관계가 있는 경우 업데이트의 순서를 적절하게 조절해야 한다.

 

15.6.3.1 단계적 업데이트

하부 시스템 간 의존성을 제대로 처리하려면 메인 게임 루프 안에서 하부 시스템들이 올바른 순서대로 업데이트하도록 명확하게 코드를 짜면 된다. 예를 들어 래그 돌 물리 시스템이 상호작용하는 과정을 올바로 처리하려면 다음과 같이 코드를 짤 수 있다.

while(true) // 메인 게임 루프
{
    // 
    /* 모든 객체의 상태 설정 */
    //
    
    g_animationEngine.CalculateIntermediatePoses(dt); // 중간 단계의 로컬 공간 뼈대 포즈를 계산
    g_ragdollSystem.ApplySkeletonsToRagDolls(); // 월드 공간으로 다시 계산하여 물리 시스템 내의 연결된 강체들에 적용
    g_physicsEngine.Simulate(dt); // 래그 돌의 물리 시뮬레이션 동작
    g_collisionEngine.DetectAndResolveCollisions(dt);
    g_ragdollSystem.ApplyRagDollsToSkeletons(); 
    g_animationEngine.FinalizePosAndMatrixPalette(); // 최종 월드 공간 포즈를 계산하고 스키닝 행렬 팔레트를 생성
    
    // ..
}

 

이렇게 게임 객체들이 다양한 엔진 하부 시스템의 중간 결과에 의존하는 경우 게임 객체들의 상태를 언제 업데이트 해야 하는지를 고민해야 한다.

 

15.6.3.2 버킷 업데이트

객체간 의존성이 있는 경우 문제가 생길 수 있다.

 

객체 B가 객체 A의 손에 들려 있어서 A가 완전히 업데이트 된 후(최종 월드 공간 포즈와 행렬 팔레트까지 계산이 끝난 후)에야 B를 업데이트 할 수 있다고 가정한다.

 

해결하는 방법은 객체들의 의존성 트리를 만들어 의존이 없는 객체(첫번 째 버킷) 들의 그룹(버킷)들의 완전한 게임 객체 업데이트 및 엔진 시스템을 업데이트 한 뒤 차례대로 버킷을 업데이트 하는 것이다.

 

의존성 트리들의 깊이를 제한하는 게임이 많다. (고정된 수의 버킷을 쓰기 위해서)

 

15.6.3.3 불완전한 객체 상태와 한 프레임 차 랙

게임 객체의 상태는 순차적으로 설정되기 때문에 루프 중간에 프로그램을 잠시 멈추면 어떤 객체들은 업데이트 되지 않아, S(t₁)로, 어떤 객체들은 업데이트 되어 S(t₁ + Δt)의 상태를 갖게 된다. 또한 루프의 어느 지점이느냐에 따라 객체들이 모두 일부만 업데이트된 상태일 수도 있다. 예를 들어 애니메이션 포즈 블렌딩은 이미 수행했지만, 물리와 충돌 과정은 아직 계산되지 않았을 수도 있다.

 

게임 객체들의 상태는 업데이트 루프 전과 후에는 일관되지만 루프 도중에는 일관되지 않을 수도 있다는 뜻이다.

 

문제는 주로 업데이트 루프 안에서 게임 객체들이 서로의 상태 정보를 질의할 때 발생한다.

이런 문제를 해결하려면 게임 객체를 버킷으로 그룹 짓는 방법도 있지만 객체 상태 캐싱이라는 기법을 사용할 수도 있다.

 

15.6.3.4 객체 상태 캐싱

새로운 객체 상태 벡터 S(t₂)를 계산하기 전에 이전 객테 상태 벡터 S(t₁)을 복사(캐싱)해 놓게 만드는 방법이다.

ㄴ 이전 객체이기 때문에 업데이트 순서에 상관이 없고 일관성이 있다.

ㄴ 이전 시각과 현재 시각 사이의 아무 때라도 두 상태를 선형 보간하여 얻을 수 있다. (하복의 물리엔진은 모든 강체의 이전 상태와 현재 상태를 모두 유지한다.)

 

단점

ㄴ 상태를 엎어 쓰는 방식에 비해 메모리를 두 배 더 차지한다.

ㄴ 이전 시각 t₁의 상태는 일관되지만 현재 시각 t₂의 상태는 일관성 문제가 발생할 수 있다.

 

15.6.4 병렬성을 위한 디자인

게임 객체 상태를 업데이트 하는 방식에 어떤 방식으로 병렬화를 사용할 것인가?

 

 

15.6.4.1 게임 객체 모델 자체를 병렬화

게임 객체 모델은 병렬화하기 어렵다.

ㄴ 게임 객체들은 서로 의존성이 강하다.

 

게임 객체 모델을 병렬화 한다면

ㄴ 모든 게임 객체들은 다른 게임 객체들을 참조할 수 없어야 한다. 모든 객체 간 통신은 메세지 전달 방식을 써야 하며, 객체들이 완전히 다른 메모리 영역에 있거나 물리적으로 분리된 CPU 코어에서 처리되고 있는 상황에서도 효율적인 객체 간 메세지 전달 시스템을 구현해야 한다.

 

15.6.4.2 병행 구조인 엔진 하부 시스템과의 인터페이스

일반적으로는 게임 객체 모델을 병렬화하는 방법보다는 로우레벨 엔진 시스템들을 병렬화하는 데 노력을 기울인다.

ㄴ 로우레벨 하부 시스템이 게임 객체 모델 처리보다 CPU를 더 많이 쓰기 때문

 

싱글 스레드 게임 객체 모델을 사용하더라도 병렬적으로 실행되는 엔진 하부 시스템과 상호작용을 해야 한다.

ㄴ 게임 객체들을 업데이트 할 때 블로킹 함수를 호출하면 안 된다.

ㄴ 작업 요청을 가장 일찍 보낼 수 있는 때는 언제인가? (일찍 보내야 필요할 때 완료되어있을 가능성이 커진다.)

ㄴ 작업 요청을 보내고 얼마나 오래 기다릴 수 있는가? (업데이트 루프 뒷부분까지 기다리거나 몇 프레임 랙이 생겨도 상관 없어서 이전 프레임의 결과로 현재 프레임의 객체 상태를 업데이트 할 수도 있다.) 

 

15.7 이벤트와 메세지 전달

게임은 본질적으로 이벤트 주도 방식으로 돌아간다.

이벤트가 발생했음을 연관있는 게임 객체들에게 알려주고, 그 객체들이 이벤트에 반응할 수 있게 도와 줄 방법이 필요하다.

15.7.1 정적 타입 함수 바인딩의 문제점

게임 객체에게 이벤트가 발생했음을 알리는 가장 간단한 방법은 그 객체의 멤버함수를 부르는 것이다.

 

정적 타입 가상 함수를 이벤트 핸들러로 사용하려면 베이스 클래스 GameObject에는 게임에서 발생 가능한 모든 이벤트 핸들러를 가상 함수로 선언해야 한다. (데이터 주도 방식으로 이벤트를 생성할 수 없다.)

 

이벤트에 관심이 있는 일부 객체 타입이나 일부 인스턴스들만 그 이벤트를 받게 하고 싶어도 방법이 없다.

 

동적 타입 함수 바인딩을 사용하면 된다. 이 기능을 기본적으로 지원하는 프로그래밍 언어도 있다. (C#) 그렇지 않은 경우에는 직접 코드를 짜야 한다. 

 

보통 데이터 주도형 접근 방식으로 구현한다. (함수 호출 개념을 객체로 캡슐화한 후 이것을 런타임에 전달하는 방식)

 

15.7.2 이벤트를 객체에 캡슐화

이벤트는 두 가지 구성 요소로 이뤄진다. 하나는 타입(폭발, 아군의 부상, 체력 아이템을 집어 들기) 이고, 다른 하나는 전달 인자이다.

 

공통된 루트 이벤트 클래스를 상속하여 여러 다른 타입의 이벤트를 구현하기도 한다.

 

이벤트 (메세지) 를 객체로 캡슐화 하면 여러 이득이 있다.

ㄴ 단일 이벤트 핸들러 함수 : 모든 타입의 이벤트를 처리할 가상 함수 한 개만 있으면 된다. (virtual void OnEvent(Event& event);)

ㄴ 영속성 : 함수 호출에서는 함수가 리턴하고 나면 인자들을 사용할 수 없지만 이벤트 객체는 타입과 전달 인자를 데이터로 저장하기 때문에 영속성이 있다.

ㄴ 이벤트 전달의 임의성 : 객체는 연관된 객체에 이벤트를 전달할 수 있는데 전달하는 객체는 이벤트에 대해 몰라도 된다.

 

struct Event
{
    const U32 MAX_ARGS = 8;
    EventType m_type;
    U32 m_numArgs;
    EventArg m_aArgs[MAX_ARGS];
};

 

이벤트/메세지/명령을 객체로 캡슐화 한다는 개념은 명령 패턴이라고 불린다.

https://lemonyun.tistory.com/66

 

2. 디자인 패턴 - 명령(command)

명령 패턴 - 간단하게 정리하자면 함수 호출을 매개변수화 한 것? 콜백 함수, 함수 포인터를 사용하여 구현할 수 있다. 2.1 입력키 변경 버튼을 눌렀을때 특정 메서드(행동)가 수행하도록 하는 코

lemonyun.tistory.com

 

15.7.3 이벤트 타입

// 이벤트 타입을 열거형 타입으로 정의해 구분하는 방법
enum EventType
{
    EVENT_TYPE_LEVEL_STARTED,
    EVENT_TYPE_PLAYER_SPAWNED,
    EVENT_TYPE_ENEMY_SPOTTED,
    // ...

};

이 방식의 문제점

1. 게임 전체에 쓰이는 모든 이벤트 타입에 대한 정보가 한 곳에 집중돼 있음

2. 이벤트 타입이 하드 코딩되어 있기 때문에 데이터 주도 방식으로 쉽게 추가할 수 없음

3. 열거형 값은 단순한 인덱스에 불과해서 순서에 영향을 받는다.

ㄴ 이벤트 타입을 문자열로 인코딩하는 방법을 사용하면 해결할 수 있다.

 

15.7.4 이벤트 전달 인자

이벤트 전달 인자를 구현하는 방법

1. 각 이벤트 타입마다 새 이벤트 클래스를 상속하고 전달 인자는 새 이벤트 클래스의 데이터 멤버로 하드 코딩

 

2. variant의 집합으로 저장하는 방법

ㄴ 고정 크기의 작은 배열로 구현

ㄴ 동적 크기 배열이나 연결 리스트를 이용해 구현

 

3. 키-값 쌍을 이벤트 전달 인자로 사용

ㄴ 이벤트를 보내는 쪽과 받는 쪽에서 전달 인자의 순서를 정확하게 알고 있어야 하기 때문에 등장한 개념(버그가 발생하기 쉬움)

 

15.7.5 이벤트 핸들러 (OnEvent 함수)

게임 객체가 이벤트를 받으면 그 이벤트에 반응해야 한다. 이 과정을 이벤트를 핸들링한다고 하고, 보통 이벤트 핸들러라고 부르는 함수나 스크립트 코드에서 처리한다.

 

이벤트 핸들러는 대부분 모든 타입의 이벤트를 처리할 수 있는 네이티브 가상 함수나 스크립트 함수로 구현하는 경우가 많다.

 

virtual void SomeObject::OnEvent(Event& event)
{
    switch (event.GetType())
    {
    case EVENT_ATTACK:
        RespondToAttack(event.GetAttackInfo());
        break;
    case EVENT_HEALTH_PACK:
        AddHealth(event.GetHealthPack().GetHealth());
        break;
    //...
    
    //이 객체에서 처리하지 않는 이벤트
    default:
        break;
    }
}

 

15.7.7 책임 연쇄 패턴

게임 객체들끼리는 거의 항상 의존성이 있다.

일반적으로 게임 객체들 간의 상호 연관성은 하나 이상의 관계 그래프로 생각할 수 있다.

이와 같은 관계 그래프에서 연결된 객체들 간에 당연히 이벤트를 전달할 수 있어야 한다. 여러 개의 구성요소로 이뤄진 게임 객체가 이벤트를 받으면 구성 요소 모두에 이벤트를 전달해 처리할 기회를 줘야 하는 경우도 있다.

 

객체들로 이뤄진 그래프 내에서 이벤트를 전달하는 기법은 책임 연쇄 패턴이라고 불리기도 한다.

이벤트가 전달되는 순서는 보통 엔지니어가 정하며, 이벤트의 체인이 첫 번째 객체에 전달하면 그 객체의 이벤트 핸들러에서 처리될 수 있는지 판단한다. (switch문의 case에 걸리지 않으면 다음 객체에 전달)

 

15.7.8 관심 이벤트 등록

이벤트를 브로드캐스팅하면 모든 게임객체의 이벤트 핸들러를 호출해야 한다. (비효율적)

 

게임 객체들이 특정 이벤트에 관심 등록을 할 수 있게 하는 방법

1. 이벤트 타입마다 관심 등록한 게임 객체를 연결 리스트로 두기

2. 게임 객체에 이벤트를 나타내는 비트 배열을 두고 관심 있는 것만 비트를 켜는 방법

 

15.7.9 이벤트 큐 사용 여부

15.7.9.1 이벤트 큐 방식의 몇 가지 장점

 

이벤트 처리 시점 제어

ㄴ 이벤트들은 게임 루프 안에서 어느 시점에 처리되는지에 민감한 영향을 받는 경우가 있다.

 

미래에 이벤트를 보내기

ㄴ 큐에 넣기 전에 원하는 전달 시각을 이벤트에 기록한다. 그리고 현재의 게임 클록이 이벤트의 전달 시각과 같거나 이미 지났을 경우만 이벤트를 처리하게 한다. 큐의 이벤트들을 전달 시각이 이른 순으로 정렬해 놓으면 이런 식으로 동작하도록 구현하기 수월하다.

 

이벤트 우선 순위

ㄴ 여러 이벤트가 같은 전달 시각을 갖는 경우 이벤트의 우선순위 레벨을 주어 해결할 수 있다.

 

15.7.9.2 이벤트 큐 방식의 문제점

이벤트 시스템의 복잡도 증가 (구현 난이도, 유지보수 비용 증가)

이벤트와 전달 인자를 깊은 복사해야 하는 문제

깊은 복사는 동적 메모리 할당(잠재적인 속도 저하와 메모리 단편화 문제)를 해야 할 수 도있게 만든다.

 

15.7.10 이벤트를 즉시 보낼 때의 문제점 (큐를 사용하지 않는 경우의 문제점)

엄청나게 깊은 콜 스택을 유발할 수 있다.

 

15.7.11 데이터 주도 이벤트/메세지 전달 시스템

이벤트 시스템을 데이터 주도 방식으로 구현하면 이벤트를 보내고 받는 과정을 수정할 권한을 디자이너에게 줄 수 있다.

 

월드 에디터에서 디자이너는 객체를 선택하고 그 객체가 특정 이벤트에 어떻게 반응할지 (어떤 핸들러를 사용할지) 설정할 수 있다.

엔진에서 게임 디자이너가 쓸 수 있게 간단한 스크립트 언어를 지원하는 형태가 있다. 디자이너는 특정 타입의 객체가 특정 타입의 이벤트에 어떻게 반응할지를 코드로 짤 수 있다.

 

15.7.11.1 데이터 통로 통신 시스템

모든 게임 객체들이 데이터 스트림을 연결할 수 있는 입력 포트를 하나 이상 갖고, 다른 객체들에 데이터를 보낼 출력 포트를 하나 이상 갖는다.

 

디자이너가 원하는 행동을 구현할때 GUI를 이용해 포트들을 연결할 수 있다.

 

15.8 스크립트

스크립트는 바이트코드 패턴과 연관이 있다.

https://lemonyun.tistory.com/75

 

11. 행동 패턴 - 바이트코드

가상 머신 명령어를 인코딩한 데이터로 행동을 표현할 수 있는 유연함을 제공한다. 큰 규모의 프로젝트는 빌드에 시간이 오래 걸릴 수 있기 때문에, 자주 고칠 수 있어야 하는 코드는 데이터 파

lemonyun.tistory.com

15.8.1 런타임 언어와 데이터 정의 언어

스크립트 언어가 사용되는 용도

1. 데이터 정의 언어

ㄴ 나중에 엔진에서 사용될 새 자료구조를 만들고 채워 넣는 것을 도와주는 것이 주된 목적

 

2. 런타임 스크립트 언어

ㄴ 런타임에 엔진 안에서 실행되는 스크립트 언어로, 대개 하드코딩된 엔진의 게임 객체 모델이나 다른 엔진 시스템을 확장하고 커스터마이즈하는데 쓰인다.

 

15.8.2.1 게임 스크립트 언어의 일반적 특성

 

인터프리트 방식

ㄴ 유연성, 이식성, 빠른 반복 생산을 위한 선택이다.

ㄴ 코드가 직접 CPU에서 실행되는 것이 아니라 가상 머신에서 실행되기 때문에 스크립트 코드를 실행하는 방법과 시점을 엔진에서 능동적으로 조정할 수 있다.

 

가벼움

ㄴ 가상머신은 메모리를 적게 먹는다.

 

편리성과 사용 편의성

ㄴ 스크립트 언어는 대개 특정 게임의 요구 사항에 맞게 커스터마이즈해 사용한다.

 

15.8.3 널리 쓰이는 게임 스크립터 언어

상용 혹은 오픈소스 언어를 고쳐 쓸 것인가 아니면 처음부터 새로 만들 것인가?

 

대부분의 경우 어느 정도 잘 알려지고 안정된 스크립트 언어를 골라 필요한 기능들을 추가해 가는 편이 더 편리하다.

 

15.8.3.1 QuakeC

퀘이크 엔진을 위해 만든 커스텀 스크립트 언어

 

15.8.3.2 언리얼스크립트

C++와 문법 구조가 유사하고 클래스, 지역변수, 루프, 배열, 구조체 등 C와 C++ 프로그래머가 익숙한 대부분의 개념을 지원한다.

 

클래스 계층 구조를 확장할 수 있는 능력

레이턴스 함수

언리얼에디터와의 편리한 연결

멀티플레이어 게임을 위한 네트워크 리플리케이션

 

15.8.3.3 루아

게임 스크립트 언어로서 제일 많이 쓰인다고 한다.

 

15.8.3.4 파이썬

15.8.3.5 폰/스몰/스몰-C

 

15.8.4 스크립트의 구조

스크립트 콜백

ㄴ 게임 루프에서 객체를 업데이트할 때 스크립트로 작성된 콜백 함수를 엔진에서 호출할 수 있다. 이를 통해 시간에 따른 게임 객체를 어떻게 업데이트 할 것인지 커스터마이징 할 수 있다.

 

스크립트로 짠 이벤트 핸들러

 

스크립트로 게임 객체 타입을 확장하거나 새 타입을 정의하기

 

스크립트로 구성 요소 및 속성 정의하기

 

스크립트 주도 엔진 시스템

ㄴ 게임 객체 모델 전부를 스크립트로 짜고, 네이티브 엔진 코드는 로우레벨 엔진 구성 요소를 사용해야 할 때만 호출하는 방식으로 구현한다.

 

스크립트 주도 게임

 

15.8.5 런타임 게임 스크립트 언어의 기능

대다수 게임에서 스크립트 언어를 사용하는 주된 이유는 게임플레이 기능을 구현하는 것이고, 이것은 게임의 객체 모델을 보강하거나 커스터마이즈하는 형태로 이뤄지는 경우가 일반적이다.

 

 

15.8.5.1 네이티브 프로그래밍 언어와의 인터페이스

게임 엔진이 스크립트 코드를 실행할 수 있어야 하고, 마찬가지로 스크립트 코드가 엔진에서의 작업을 시작하게 할 수 있어야 한다.

 

보통 런타임 스크립트 언어의 가상 머신은 게임 엔진 안에 들어간다. 엔진이 가상 머신을 초기화하고 필요한 때에 스크립트 코드를 실행하며, 스크립트가 실행되는 과정을 관리한다.

 

함수형 스크립트 언어의 주요 실행 단위는 함수다. 함수 이름이 위치한 바이트 코드를 찾은 후 가상 머신을 생성해 코드를 실행한다.

 

객체지향 스크립트 언어에서는 클래스가 주요 실행 단위다. 이런 시스템에서는 객체를 생성하거나 파괴하는 것이 가능하고, 각 클래스 인스턴스의 메서드를 호출할 수 있다.

 

15.8.5.2 게임 객체 핸들

 

스크립트 함수가 게임 객체를 다뤄야 하는 때도 있는데 스크립트 코드에서 게임 객체를 참조할 방법은 여러가지가 있다.

임의의 숫자로 이뤄진 핸들을 사용하는 방법이 있다. (혹은 문자열, 해시 문자열 id)

 

15.8.5.3 이벤트 받기와 처리

이벤트는 보통 개별 객체에 전달되고 그 객체 내에서 처리된다. 그렇기 때문에 스크립트로 짠 이벤트 핸들러를 어떤 식으로든 객체와 연관 지을 방법이 있어야 한다.

 

15.8.5.4 이벤트 보내기

 

15.8.5.5 객체지향 스크립트 언어

 

15.8.5.6 스크립트로 짠 유한 상태 기계

 

15.8.5.7 스크립트 멀티스레드

'읽은 책 > 게임 엔진 아키텍처' 카테고리의 다른 글

14. 게임플레이 시스템의 소개  (0) 2022.08.17
12. 충돌과 강체 역학  (0) 2022.08.17
11. 애니메이션 시스템  (0) 2022.08.13
10. 렌더링 엔진  (0) 2022.08.12
9. 디버깅과 개발 도구  (0) 2022.08.10

 

14.1 게임월드의 구조

14.1.1 게임 월드의 요소

대부분의 비디오 게임들은 2차원 또는 3차원의 가상 게임월드에서 이루어지는데, 이 가상 월드는 여러 가지 개별적인 요소들로 구성된다. 일반적으로 이 요소들은 크게 정적요소와 동적요소로 나눌 수 있다.

정적 요소는 지형, 건물, 도로, 교량 등과 같이 움직이지 않거나 게임플레이와 적극적인 상호작용을 하지 않는 것들을 의미한다.

나머지들은 다 동적 요소이다.

 

일반적으로 게임플레이는 주로 동적 요소를 다루게 된다.

게임월드의 동적요소 전체를 나타내는 용어로서 게임 상태 (game state)라는 용어를 사용하기로 하자

 

정적요소와 동적요소를 구분하게 되면 최적화에 활용할 수 있다. 예를 들어 어떤 메쉬가 정적이어서 움직이지 않는다면 정적 정점 조명, 조명 맵, 그림자 맵, 정적 환경 차폐 정보, PRT, 구면 조화 함수 계수를 사용한 방식으로 조명을 미리 계산할 수 있다.

 

14.1.1.1 정적 기하

정적 요소의 기하는 주로 마야(Maya)와 같은 도구를 사용해 만드는데, 커다란 하나의 삼각형 메시로 만들거나 또는 여러 개의 작은 조각으로 분해하여 만들게 된다. 화면의 정적인 부분은 인스턴싱된 기하를 이용해 만드는 경우도 있다.

 

정적 시각요소와 충돌 데이터(충돌 기본 단위)는 브러시 기하를 이용하여 만들 수 있다. 브러시는 여러 볼록 다면체들이 모여서 만들어진 모양을 의미하며, 브러시 기하는 빠르고 만들기 쉬우며 BSP 트리를 기반으로 하는 렌더링 엔진에 잘 통합될 수 있다. 

 

14.1.2 월드 청크

게임 월드는 메모리의 제한, 게임흐름 조절의 필요성, 개발 과정에서 분업의 필요성 등의 이유에 따라 청크로 나누어진다.

 

14.1.3 하이레벨 게임흐름

하이레벨 게임흐름이란 플레이어의 목표를 선형적으로 또는 트리나 그래프와 같은 형태로 정의하는 것이다.

 

14.2 동적요소 구현하기 : 게임 객체

게임 월드 내의 동적요소들을 게임 객체라고 부르기로 한다.

게임 객체는 개체(entities), 액터(actors), 에이전트 (agent) 등 여러 가지 이름으로 불리고 있다.

 

게임 객체는 보통 타입에 따라 분류된다. 타입이 다른 객체들은 속성 스키마와 행위도 서로 다르다.

 

14.2.1 게임 객체 모델

게임 객체 모델은 특정 게임을 구성하는 개체들을 시뮬레이트하기 위하여 사용되는 구체적인 객체지향 프로그래밍 인터페이스다.

 

게임 객체 모델은 게임 엔진을 만들 때 사용한 프로그래밍 언어를 확장하는 경우가 많다.

ㄴ C++언어와 같은 객체지향 언어로 만들어진 게임의 경우에도 리플렉션, 지속성, 네트워크 복제 같은 고급 기능들을 추가할 수 있다.

ㄴ 게임 객체 모델은 여러 언어의 기능들을 혼합하기도 한다. 예를 들어 C언어 C++ 언어같이 컴파일이 필요한 프로그래밍 언어와 Python, Lua, Pawn 같은 스크립트 언어를 통합하여 양쪽 언어에서 모두 사용할 수 있는 단일화된 객체 모델을 제공할 수도 있다.

 

14.2.2 툴 측면 설계와 런타임 설계

기획자가 월드 에디터를 통해 보는 객체 모델이 런타임에 게임을 구현하기 위해 사용되는 객체 모델과 다를 수도 있다.

ㄴ 툴 측면 게임 객체 모델은 런타임에 C언어와 같은 객체지향 기능이 없는 언어로 구현될 수도 있다.

ㄴ 직관적으로는 하나의 툴 측면 게임 객체가 런타임에 하나의 클래스로 구현되어야 할 것 같지만 필요에 따라 여러 클래스의 모음으로 구현될 수도 있다.

ㄴ 툴 측면 게임 객체는 런타임에 고유한 id 하나가 되는 경우도 있다.

 

툴 측면 객체 모델은 기획자가 월드 에디터를 사용하면서 보게 되는 게임 객체 타입들이다.

런타임 객체 모델은 프로그래머가 툴 측면 객체 모델을 구현할 때에 사용한 프로그래밍 언어와 소프트웨어 시스템으로 규정된다.

 

14.3 데이터 주도 게임 엔진

데이터 주도 게임 엔진이라는 것은 게임의 전체 혹은 일부가 프로그래머가 개발한 소프트웨어 보다는 아티스트나 기획자가 만들어 낸 데이터에 의해 조정된다는 것을 의미한다.

ㄴ 모든 팀원들의 잠재력을 최대한 이끌어내고, 엔지니어들의 부담을 경감시킬 수 있기 때문에 개발팀의 효율을 개선할 수 있다.

ㄴ 게임 기획자와 아티스트들에게 데이터 주도적인 방식으로 게임 콘텐츠를 만들 수 있는 툴이 제공되어야 한다.

ㄴ 게임 기획자와 아티스트들이 자신들의 작업을 게임 속에서 미리 살펴보고 문제를 해결할 수 있는 도구도 제공되어야 한다.

 

데이터 주도 구조가 특정 게임을 기획하는데 미치는 영향이나 팀원들의 구체적인 요구사항들을 고려해보지도 않은 채 데이터 주도 구조를 만들려고 시도한다면 필요 이상으로 목표를 높게 잡거나, 너무 복잡하고 사용하기 힘든 툴과 엔진 시스템을 만들게 될 수 있다. 

 

14.4 게임월드 에디터

게임 플레이 공간에서 게임 월드 청크를 정의하고, 정적 요소와 동적 요소들을 생성하는 툴에 해당하는 것이 게임월드 에디터이다.

모든 상용 게임엔진에는 어떤 형태로든 월드 에디터 툴이 포함되어 있다.

일반적으로 게임월드 에디터에서는 속성값과 같은 게임 객체의 초기 상태를 지정할 수 있다. 또한 사용자가 게임월드 내에서 동적요소의 행동을 조절할 수도 있다. 이러한 작업은 데이터 주도적 설정 파라미터를 이용할 수도 있고, 스크립트 언어를 이용할 수도 있다.

14.4.1 게임월드 에디터의 일반적인 기능

 

14.4.1.1 월드 청크의 생성과 관리

게임월드는 보통 청크 단위로 만든다.

게임월드 에디터를 사용하여 새로운 청크를 생성할 수 있고, 기존의 청크에 대하여 이름을 바꾸거나 분해하거나 합치거나 없앨 수 있다. 각 청크들은 여러 개의 정적 메시나 AI 네비게이션 맵, 잡을 수 있는 물체에 대한 정보, 방어 지점에 대한 정보 등과 같은 여러 정적요소들과 연결될 수 있다.

 

하나의 청크를 하나의 배경 메쉬로 정의하는 엔진에서는 배경 메쉬가 없이는 청크를 만들지 못한다.

어떤 게임 엔진에서는 청크를 AABB, OBB, 임의의 다격형 영역과 같은 경계 볼륨으로 청크를 정의하고 그 안에 여러 개의 메시를 넣는다.

월드 에디터 중에는 지형이나 물과 같은 특별한 정적요소들을 저작하기 위한 전용 도구가 있는 것도 있다.

 

14.4.1.2 게임월드의 시각화

게임월드 에디터의 사용자(기획자, 아티스트)에게는 게임월드 콘텐츠를 시각화 하는 것이 중요하다. 그래서 대부분의 게임 월드 에디터에서는 3차원 투영기능이나 2차원 직각투영 기능을 제공한다.

 

어떤 에디터에서는 전문 렌더링 엔진을 직접 포함하고 있어서 이러한 시각화 기능을 직접 제공하기도 하고, 또 다른 에디터는 maya나 3ds Max와 같은 3D기하 에디터의 뷰포트 기능을 이용하여 시각화 기능을 제공하기도 한다.

 

14.4.1.3 네비게이션

사용자가 게임월드 내에서 물체를 배치하기 위해서는 카메라를 이동시킬 수 있어야 한다.(네비게이션)

직각 투영, 스크롤, 줌인/아웃, 특정 객체 주위 회전 기능이 필요할 수 있다.

 

14.4.1.4 선택

한 번에 하나의 객체를 선택할 수 있는 에디터도 있지만 더 발전된 에디터는 여러 객체들을 한번에 선택할 수 있다.

 

객체를 선택할 때는 직각 투영 화면에서 고무줄 박스를 이용할 수도 있고, 3D 화면에서 레이 캐스트 선택 기능을 이용할 수도 있다.

 

3D 화면에서 레이 캐스트를 이용하여 객체를 선택할 때 가장 가까운 객체를 선택하여 주는 대신 레이와 교차되는 객체들을 순서대로 번갈아 가면서 선택하여 주는 기능을 제공하는 에디터도 있다.

 

모든 월드 요소들의 이름을 스크롤 할 수 있는 형태의 리스트나 트리 형태로 보여주어 객체를 선택할 수 있도록 하는 에디터들도 있다.

 

14.4.1.5 계층

게임 객체들을 모아서 계층별로 관리하면 게임 월드의 컨텐츠들을 잘 정리할 수 있다.

계층별로 화면에서 보이거나 보이지 않게 설정할 수도 있고, 계층별로 색깔을 부여해 구별할 수도 있다.

분업을 할 때도, 계층을 이용할 수 있다.

각 계층들을 개별적으로 불러오거나 저장할 수 있는 에디터에서는 여러 사람이 동시에 하나의 월드 청크에 대한 작업을 하더라도 충돌이 발생하지 않도록 할 수 있다.

 

14.4.1.6 속성 그리드

게임 월드 청크에 있는 정적 및 동적요소에는 사용자가 수정할 수 있는 여러 가지 속성이 있다.

단순한 키-값의 쌍으로 정의되며 대부분의 에디터에서는 스크롤 할 수 있는 속성 그리드 창을 이용하여 현재 선택된 객체의 속성들을 보여준다. 사용자는 속성 그리드 창에서 직접 타이핑을 하거나 체크박스, 드롭다운 콤보박스를 이용하거나 스피너 컨트롤을 상하로 드래깅하여 값을 수정할 수도 있다.

 

14.4.1.7 객체의 위치 정하기와 정렬시키기

객체의 이동, 회전, 크기 변화 기능을 제공해야 한다.

객체를 특정위치에 배치하거나 정렬시키는 기능을 제공한다.

ㄴ 자석 기능 (snap to grid)

ㄴ 객체 기준으로 정렬하기 (align to object)

 

14.4.1.8 특수 객체 타입

조명

ㄴ 조명에는 메시가 없기 때문에 대개 특수한 아이콘을 이용하여 조명을 표현한다.

 

파티클 이미터

ㄴ 렌더링 엔진이 에디터와 독립되어 있다면 에디터에서 파티클을 시각화하는데 문제가 있다. 때문에 아이콘만을 사용해서 파티클 이미터를 보여준다.

 

음원

ㄴ 3D 렌더링 엔진은 음원도 3차원 입체로 모델링한다. 이러한 작업을 위해서 월드 에디터에서 전용 편집 툴을 제공하는 경우가 많다.

 

영역(트리거)

ㄴ 게임에서 객체가 어떤 공간에 들어가거나 나오는 등의 일이 발생했을 때에 이를 감지하거나 여러 가지 목적으로 공간을 나눌 때에 사용하는 입체 공간이다.

 

스플라인

ㄴ 스플라인이란 여러 개의 조절점들과 그 점에서의 접선 벡터로 정의되는 3차원 곡선이다. 

ㄴ 대개는 접선 벡터 없이 조절점으로만 정의되고 곡선이 모든 조절점들을 지나는 특성을 가지는 Catmull-Rom 스플라인이 많이 사용된다.

ㄴ 월드 에디터에서는 스플라인을 구성하는 개별 조절점을 선택하여 조정할 수 있는 기능과 객체(곡선) 전체를 선택하는 기능이 필요하다.

 

AI를 위한 네브 메시(nav meshes)

ㄴ 많은 게임에서 NPC들은 경로 찾기 알고리즘을 사용하여 게임월드의 탐색 가능 영역을 돌아다닌다. 이 때 AI 설계자들이 월드 에디터를 사용하여 탐색 가능 영역을 생성, 시각화, 수정한다. 예를 들어 네브 메쉬는 탐색 가능 영역의 경계를 쉽게 정의할 수 있도록 하는 2D 삼각형 메시로서 경로 검색기에게 연결 정보를 제공한다.

 

14.4.1.9 월드 청크의 저장 및 불러오기

불러오고 저장하는 월드 청크의 단위는 엔진마다 크게 다르다.

ㄴ 월드 청크 하나를 하나의 파일로 저장하는 엔진

ㄴ 계층을 개별적으로 저장하는 엔진

 

데이터 포맷도 엔진마다 다르다.

ㄴ 이진 포맷

ㄴ XML이나 JSON같은 텍스트 포맷

 

14.4.1.10 빠른 반복작업

어떤 에디터는 게임 속에서 직접 수행되면서 사용자가 수정한 효과를 즉시 확인할 수 있게 해준다.

 

오프라인으로 동작하는 에디터는 마야의 플로그인 형태로 동작하는 프로그램을 예로 들 수 있다.

'읽은 책 > 게임 엔진 아키텍처' 카테고리의 다른 글

15. 런타임 게임플레이 기반 시스템  (0) 2022.08.20
12. 충돌과 강체 역학  (0) 2022.08.17
11. 애니메이션 시스템  (0) 2022.08.13
10. 렌더링 엔진  (0) 2022.08.12
9. 디버깅과 개발 도구  (0) 2022.08.10

 

12.1 게임에서 물리가 필요한가?

밧줄, 머리카락, 의복 등에 물리적인 동작을 더하면 독특한 게임 효과를 낼 수 있다.

 

12.1.1 물리 시스템으로 할 수 있는 일

ㄴ 동적 객체와 정적 게임 월드 간의 충돌을 검출

ㄴ 중력이나 기타 힘들의 영향을 받아 자유롭게 움직이는 강체 시뮬레이션 스프링 질량계 (spring-mass-system)

ㄴ 부서지는 빌딩과 구조물

ㄴ 레이 캐스트(ray cast) 와 형상 캐스트(shape cast)

ㄴ 트리거 볼륨(Trigger Volume : 게임 월드의 지정된 지역에 물체가 진입하거나 떠나는 순간을 비롯해 그 안에 자리하고 있는지 등을 판별)

ㄴ 캐릭터가 단단한 물건을 집어 드는 기능

ㄴ 복잡한 기계류 (크레인, 움직이는 플랫폼 퍼즐 등)

ㄴ 래그 돌

ㄴ 움직일 수 있는 소품들과 사실에 근접한 머리카락 및 의복의 움직임

ㄴ 물 표면 시뮬레이션과 부력

ㄴ 오디오 전달

 

게임에서 런타임에 물리 시뮬레이션을 수행하는 방법 외에도 오프라인 전처리를 통해 애니메이션 클립을 만드는 과정에서도 물리 시뮬레이션을 응용할 수 있다.

 

12.1.2.1 시뮬레이션 게임

시뮬레이션 게임의 주 목적은 가능한 한 실제와 가까운 경험을 재현하는 것이기 때문에 강체 역학 시스템을 이용해 얻을 수 있는 사실감은 게임에 큰 도움이 된다.

 

12.1.2.2 물리 퍼즐 게임

물리 퍼즐 게임의 핵심은 게이머가 역학적으로 시뮬레이션되는 장난감들을 이리저리 갖고 놀게 하는 것이다. 물리가 꼭 필요하다.

 

12.1.2.3 샌드박스 게임

샌드박스 게임에서 플레이어의 주 목적은 이리저리 돌아다니면서 게임 월드의 물건들로 무엇을 할 수 있는지 탐험하는 것이다. 재미를 위해 사실성을 다소 희생하는 경우도 많다. (실제보다 과하게 묘사)

 

12.1.2.4 목적 중심 게임과 스토리 중심 게임

물리 시스템과는 잘 맞지 않는 편이다.

 

12.1.3 물리가 게임에 미치는 영향

12.1.3.1 디자인에 미치는 영향

ㄴ 예측 가능성을 저해한다.

ㄴ 튜닝과 컨트롤이 어렵다. 

 

원칙적으로는 게임 디자인을 먼저 고르고 물리 시뮬레이션을 넣을지 말지 정해야 한다.

 

12.1.3.2 엔지니어링에 미치는 영향

툴 파이프라인

ㄴ 품질이 뛰어난 충돌/물리 파이프라인을 만들고 관리하는데 적잖은 시간이 걸린다.

 

유저 인터페이스

ㄴ 플레이어가 게임 월드의 물리 객체를 어떻게 컨트롤 할 것인지를 정해야 한다.

 

충돌 검출 역학

ㄴ 시뮬레이션에 사용될 충돌 모델은 그렇지 않은 모델보다 정교하고 조심해서 만들어야 한다.

 

AI

ㄴ 물리적으로 시뮬레이션되는 물체들이 섞여 있으면 길 찾기 결과를 정확히 예측하기 힘들 수 있다.

 

오동작하는 물체

ㄴ 물체들끼리 서로 살짝 뚫고 나갔을 때 의도치 않게 튕겨 나가거나 심하게 요동칠 수 있다.

 

래그 돌 물리

ㄴ애니메이션에 의해 캐릭터 신체의 일부가 다른 충돌 볼륨을 뚫고 들어갈 수 있는데, 이 상태에서 래그 돌이 되면 매우 불안정한 상태를 야기할 수 있다.

 

그래픽

ㄴ 물리에 의해 발생하는 움직임은 화면에 그려지는 물체의 경계 볼륨을 변화시킬 수 있다.

ㄴ 미리 계산된 조명이나 그림자를 쓰기 어려울 수 있다.

 

네트워크와 멀티 플레이어

ㄴ 게임 플레이에 전혀 영향이 없는 물리 효과라면 각 게임 클라이언트에서 따로 계산해도 되지만, 게임 플레이에 영향을 미치는 물리, 예를 들면 수류탄의 궤적 같은 경우는 서버에서 계산한 후, 각 클라이언트에서 정확히 재현해야 한다.

 

녹화와 재생

ㄴ 역학 시뮬레이션과 녹화 재생 기능을 함께 구현하는 것은 어렵다. 시뮬레이션의 무질서한 특성과 물리 계산의 업데이트 타이밍이 달라질 경우 다시 재생한 장면이 원래 녹화된 것과 달라질 수 있기 때문이다.

 

12.1.3.3 아트에 미치는 영향

추가적인 도구와 작업 과정에 의한 복잡도 증가

ㄴ 역학 시뮬레이션에 사용될 물체의 질량, 마찰 계수, 제약조건 및 기타 속성들은 아트 팀의 작업을 복잡하게 만든다.

 

더 복잡한 컨텐츠

ㄴ 외형은 똑같지만 충돌 및 역학 설정이 다른 물체를 여러 개 만들어 다른 목적으로 써야할 수 있다.

ㄴ 예를 들어 원래 형태와 파괴할 수 있는 형태 두 가지를 만들어야 할 수도 있다.

ㄴ 물체 주도 물체는 예측 불가능할 수 있기 때문에 장면의 미적인 구성을 유지하도록 통제하기 힘들 수 있다.

 

12.1.3.4 기타 영향

ㄴ 엔진지어링, 아트, 기획 부서의 긴밀한 협업이 필요하다. (= 프로젝트의 개발 비용이 증가한다.)

 

12.2 충돌/물리 미들웨어

12.2.1 I-Collide, SWIFT, V-Collide, RAPID

I-Collide (SWIFT로 대체됨)

ㄴ 노스 캐롤라이나 대학에서 개발한 오픈소스 충돌 검출 라이브러리

ㄴ 볼록 다면체들 간의 교차를 검출할 수 있다.

 

V-Collide, RAPID

ㄴ 복잡하지만 볼록하지 않은 다면체들도 처리할 수 있는 라이브러리

 

라이브러리들을 바로 게임 엔진에서 사용할 수는 없지만 게임에 사용할 수 있는 완전한 충돌 시스템을 구현하는데 훌륭한 기반이 될 수 있다.

12.2.2 ODE (Open Dynamics Engine)

오픈소스 충돌 및 강체 역학 SDK (공짜)

모든 소스 코드가 공개되어 있다.

 

12.2.3 불릿

게임 산업과 영화 산업에서 모두 쓰이는 오픈소스 충돌 검출 및 물리 라이브러리

충돌 엔진과 역학 시뮬레이션은 통합되어 있지만 충돌 시스템만 따로 사용하거나 다른 물리 엔진과 같이 사용할 수 있는 기능을 제공한다.

연속 충돌 검출 방식(Continuous collision detection)을 지원한다.

12.2.4 Vi TrueAxis

비상업적인 용도로는 무료로 쓸 수 있는 충돌/물리 SDK

 

12.2.5 Physx

비용을 지불하면 모든 소스코드와 라이브러리를 필요에 의해 수정할 수 있다.

 

12.2.6 하복

오늘날 상용 물리 SDK 중 가장 확고한 위치를 점하고 있으며, 가장 다양한 기능들을 지원하고 모든 플랫폼에서 훌륭한 성능을 자랑한다. (가장 비싸다)

충돌/ 물리 엔진, 차량 물리 시스템, 파괴 가능 환경 모델링 시스템, 래그 돌 물리 시스템과 통합되면서 독립적인 애니메이션 SDK 등을 지원한다.

 

12.2.7 Physics Abstraction Layer

한 프로젝트 내에서 여러 개의 물리 SDK를 사용할 수 있는 오픈 소스 라이브러리다.

 

12.2.8 Digital Molecular Matter(DMM)

한정된 방법을 사용해 모양이 변하고 파괴 가능한 물체를 시뮬레이션하는 물리 엔진이다. 오프라인 기능과 런타임 기능을 모두 지원한다.

 

12.3 충돌 검출 시스템

게임 엔진이 물리 검출 시스템을 사용하는 주된 목적은 게임 월드의 물체들이 서로 접촉을 했느냐를 알아내는 것이다.

이를 위해 검출이 필요한 모든 물체를 하나 이상의 기하적인 형태로 표현해야 한다. 충돌 시스템은 주어진 시간에 이런 형태들이 교차하는지 판별한다.

 

충돌 시스템은 단순히 교차 여부만 판별하는 것이 아니라 의미 있는 정보도 제공한다. 접촉 정보는 물체가 서로 뚫고 들어가는 부자연스러운 시각 효과를 방지하는데 쓰인다. 보통 다음 프레임을 그리기 전에 뚫고 들어간 물체들을 서로 떨어지게 만드는 방식을 이용한다.

 

충돌 시스템이 가장 중요하게 사용되는 분야는 강체 역학 시뮬레이션인데, 물체가 튕겨 나가거나 미끄러지거나 멈추는 등 사실적인 물리 현상을 재현하는 용도로 사용된다.

 

12.3.1 충돌 단위

교차 검사에서는 대개 기하학적으로나 수학적으로 단순한 형상을 선호한다. 

바위를 구로 나타낼 수도 있고 자동차의 후드는 사각 박스 형태로 나타낼 수도 있다.

 

이런 단순한 표현으로 원하는 효과를 얻을 수 없을 때만 더 복잡한 형상을 사용해야 한다.

 

하복에서 충돌 검출에 사용하는 독립된 강체를 충돌체라고 부른다. (C++ 클래스 khpCollidable의 인스턴스)

PhysX에서는 강체를 액터라고 부른다. (C++ 클래스 NxActor의 인스턴스)

 

충돌체는 두 가지 중요한 정보를 포함한다. (형상과 변환)

형상은 충돌체의 기하학적인 형태(캡슐, 구, 직육면체)를 나타내고

변환은 게임 월드 안에서의 위치와 방향을 나타낸다.

 

충돌체에 변환 정보가 필요한 이유

ㄴ 형상은 객체의 생긴 모양을 뜻할 뿐이기 때문에 제대로 쓰이려면 월드 공간에서 제 위치와 방향을 갖게 변환해야한다.

ㄴ 복잡한 형상을 구성하는 요소(여러 개의 점과 평면으로 구성된)을 일일이 움직이려면 느리기 때문

ㄴ 어느 정도 복잡한 형상을 나타내는 데 필요한 정보는 적잖은 메모리를 차지할 수 있는데, 하나 이상의 충돌체가 동일한 형상 정보를 공유할 수 있다면 변환 정보만 다르게 하여 여러 개의 충돌체를 정의 할 수 있다. 

 

게임 객체 중에는 충돌체가 전혀 없는 것도 있고 한 개를 갖고 있는 경우도 있고 여러 충돌체로 이루어진 경우도 있다.

 

12.3.2 충돌/물리 월드

일반적으로 충돌 시스템은 모든 충돌체를 관리하기 위해 충돌 월드라 불리는 싱글턴 자료 구조를 이용한다.

하복에서 충돌 월드는 hkpWorld 클래스의 인스턴스, PhysX의 충돌 월드는 NxScene의 인스턴스

충돌 월드는 게임의 모든 충돌체를 나타내는 기하학적 입체의 계층 구조에서 루트가 된다.

 

각 게임 객체 안에 충돌 정보를 넣어 관리하기보다 충돌 정보를 별도의 자료 구조로 유지하면 얻을 수 있는 장점

ㄴ 다른 객체와 충돌할 가능성이 있는 객체의 충돌체만 충돌 월드에 갖고 있으면 된다.

ㄴ 충돌체를 최대한 효율적인 방식으로 조작할 수 있는 여지가 생긴다. (캐시 미스를 최소화)

ㄴ 효과적인 캡슐화 방식으로써 동작한다.

 

12.3.2.1 물리 월드

게임에 강체 역학 시스템이 있다면 물리 시뮬레이션의 움직이는 강체는 충돌 시스템의 충돌체 하나와 연결된다. 

하복에서 강체가 없는 충돌체는 존재할 수 있지만 강체는 충돌체 하나와 대응(참조)되어야 한다.

두 SDK에서 강체의 위치와 방향을 고정시킬 수 있는데 이렇게 하면 강체는 역학 시뮬레이션에는 참여하지 않고 충돌체 역할만 한다는 뜻이다.

 

대부분의 SDK에서는 충돌 라이브러리와 강체 역학 시뮬레이션을 분리하는데 이렇게 하면 물리를 사용하지는 않지만 충돌 검출 기능은 사용하는 게임에서 이점을 얻기 때문이다.

 

12.3.3 형상의 개념

형상은 경계에 의해 둘러싸이고 분명히 안과 밖이 있는 공간이다. 게임 객체 중 많은 것들이 면으로 표현된다. (지형, 강, 얇은 벽 등) 하지만 면으로는 안과 밖을 표현할 수 없기 때문에 대부분의 충돌 SDK는 면을 기본 단위로 지원하고 형상 개념을 확장해 닫힌 공간과 열린 면을 모두 다룰 수 있게 한다.

 

일반적으로 충돌 라이브러리는 옵션으로 밀어내기 값을 주어 면이 부피를 갖게 한다. 이 값은 면이 얼마나 두꺼운지를 지정한다. 이렇게 하면 작고 빠르게 움직이는 물체가 극히 얇은 면을 지날 때 충돌을 검출하지 못하는 문제를 줄일 수 있다.

 

12.3.3.1 교차

기하학적으로 두 형상의 교차는 두 형상 안에 공통으로 들어가는 모든 점(무수히 많은) 이라 할 수 있다.

 

12.3.3.2 접촉

게임에서는 교차를 찾을 때 엄격한 정의대로 점들의 집합이 필요한 것은 아니다. 단순히 두 물체가 서로 교차하는지 아닌지만 알면 된다.

 

일반적으로 충돌 시스템은 접촉 정보를 사용하기 편한 자료 구조에 모아 놓고, 접촉이 검출될 때마다 이 자료 구조를 생성한다. 접촉 정보는 다음과 같은 것들을 포함할 수 있다.

ㄴ 분리 벡터 (물체들을 움직여 충돌 상태에서 벗어나게 만들 수 있는 벡터)

ㄴ 접촉이 발생한 두 충돌체에 관한 정보

ㄴ 분리 벡터 방향으로 물체를 움직일 속도

 

12.3.3.3 볼록함

볼록한 형상은 형상 안에서 시작한 모든 반직선이 표면을 오직 한 번만 통과하는 성질이 있기 때문에 볼록 형상끼리 교차 검사하는 것이 오목 형상이 포함될 때보다 단순하면서 계산도 쉽다.

 

12.3.4 충돌 기본 단위

12.3.4.1 구

구의 정보는 중심과 반지름으로 이루어져 부동소수 4개로 이루어진 벡터에 넣어 SIMD 수학 라이브러리에 응용할 수 있다.

 

12.3.4.2 캡슐

원기둥 하나의 양쪽에 반구가 더해진 형태, 구가 지점 A에서 지점 B로 이동하는 동안의 흔적을 나타내는 입체인 스윕구라고 볼 수도 있다.

보통 두 점과 반지름 하나를 사용한다.

원기둥이나 박스보다 교차 검사가 쉽기 때문에 사람의 팔다리와 같이 원기둥과 유사한 형태의 물체를 모델링할 때 캡슐을 많이 사용한다.

 

12.3.4.3 축 정렬 경계 박스 AABB (axis-aligned bounding box)

AABB는 위치한 좌표계에 상대적인 개념으로만 설명할 수 있다.

AABB는 두 점으로 나타낼 수 있다. (좌표축을 기준으로 박스의 가장 최소 좌표, 최대 좌표)

물체가 박스 모양이고, 물체가 좌표계의 축과 대략적으로 평행일 때에만 잘 동작할 수 있다.

 

12.3.4.4 유향 경계 박스 (OBB, Oriented Bounding Box)

축 정렬 경계 박스에 좌표축을 기준으로 회전할 수 있게 한 것

너비, 깊이, 높이와 변환(회전, 위치) 정보가 필요하다.

 

12.3.4.5 DOP (AABB와 OBB를 일반화 한 개념 : Discrete Oriented Polytope)

DOP는 여러 개의 평면을 무한대에 위치시키고, 근사하려는 물체와 만날 때 까지 법선 방향으로 움직여 만든다.

AAOB는 각 평면의 법선이 좌표축에 평행한 성질을 가진 6-DOP

OBB는 각 평면의 법선이 물체의 고유 좌표축에 평행한 성질을 가진 6-DOP

 

12.3.4.6 임의의 형상을 한 볼록 입체

대부분의 충돌 엔진들은 임의의 형상을 한 볼록 입체도 지원한다. 이런 블록 입체들은 3D 아티스트가 마야 등의 도구를 이용해 만든다. 형상의 삼각형들은 평면의 집합으로 변환되는데, (K-DOP) 이는 평면식 k개로 나타낸다.

 

 

12.3.4.7 다각형(폴리곤) 수프

임의의 형태이고 볼록하지 않은 형상을 지원하는 충돌 시스템도 있다. 이 형상들은 대개 삼각형이나 다른 단순한 다각형으로 만든다. 그래서 다각형 혹은 폴리곤 soup라고 불린다.

 

다각형 수프는 보통 지형이나 빌딩과 같이 복잡하면서 정적인 형상을 모델링할 때 쓰인다.

 

형상들의 충돌 검출 중 다각형 수프에 대한 충돌 검출이 가장 오래 걸리기 때문에 대부분의 게임에서는 역학 시뮬레이션에 참여하지 않는 물체에만 다각형 수프를 사용하게 된다.

 

볼록한 다각형이나 단순한 형상들과는 달리 다각형 수프는 닫힌 공간 뿐만 아니라 열린 표면을 나타낼 수 있다.

 

다각형 수프와 어떤 물체가 충돌 상태에 있을 때 물체를 어느 방향으로 밀어낼지 정하기 위해서 다각형 수프를 이루는 삼각형의 정점 감김 정보를 사용할 수 있다.

 

12.3.4.8 복합 형상

볼록하지 않은 물체를 모델링할 때 다각형 수프보다 복합 형상이 효율적이 경우가 많다.

 

어떤 물리 엔진(하복) 에서는 복합 형상 전체의 볼륨 경계 볼륨(하위 형상들을 포함하는 상위 형상) 을 충돌 검사에 이용해 겹치지 않는다면 하위 형상들에 대한 검사를 하지 않는 방법을 사용한다. 이 과정을 중간 단계 층돌 검사라고 부른다.

 

12.3.5 충돌 검사와 해석 기하학

12.3.5.1 점과 구

점의 중심과 구의 중심의 차이 벡터의 크기를 구의 반지름과 비교

 

12.3.5.2 구와 구

두 구의 중심의 차이 벡터의 크기와 두 반지름의 합과 비교

 

12.3.5.3 분할 축 정리

두 볼록 형상을 한 축에 투영했을 때 투영한 이미지들이 겹치지 않는 축이 존재하면, 두 형상은 교차하지 않는다.

 

어떤 형상은 분할 축이 될만한 후보를 쉽게 찾을 수 있는 특성이 있다

구와 구의 경우

ㄴ 두 구의 중심을 이은 선분과 평행한 축

 

12.3.5.4 AABB와 AABB

두 AABB가 서로 교차하는지 알기 위해서는 마찬가지로 분할 축 정리를 이용한다.

AABB의 면들은 모두 동일한 좌표축에 평행하다는 성질이 있기 때문에 분할 축이 존재한다면 3 좌표축 중 하나라는 것을 알 수 있다.

각 AABB의 각 좌표축마다 최대 최소 좌표가 모두 겹치는 경우에만 두 AABB가 교차한다고 말할 수 있다.

 

12.3.5.5 볼록 형상들 간의 충돌 검출: GJK 알고리즘

교차하는 두 블록 형상의 민코프스키 차이는 원점을 포함하지만 교차하지 않는 형상은 그렇지 않다.

GJK 알고리즘은 반복적인 알고리즘으로 처음에는 민코프스키 차이의 껍데기 위에 있는 임의의 점 하나의 단체로 시작하여 점차 차수가 높은 단체를 만들어가면서 그 중에 원점을 포함할 수 있는 것을 찾으려 한다. 지금의 단체보다 원점에 더 가까운 받침 정점 (볼록한 껍데기 위 한 점)을 단체에 더한다. 이렇게 만든 단체가 원점을 둘러싸게 되면 끝난다. (교차함)

 

12.3.5.6 기타 형상 간의 교차 검사

형상과 형상의 교차를 말할 때 형상 종류가 N개 있으면 교차 검사의 수는 N의 제곱이 되기 때문에 충돌 엔진은 기본 단위 종류의 수를 제한한다. 

하지만 GJK는 모든 종류의 블록 형상들 간의 충돌 검출을 한 번에 처리할 수 있다. (알고리즘에서 사용하는 받침 함수만 형상에 따라 다르다.)

 

하복에서는 충돌 에이전트(hkCollisionAgent 클래스를 상속받는 클래스들)라는 객체들을 이용해 교차 테스트 종류를 결정한다.

충돌 에이전트 클래스에는 (hkpSphereSphereAgent, hkpSphereCapsuleAgent, hkpGskConvexConvexAgent) 등이 있다.

hkpCollisionDispatcher 클래스의 역할은 2차원 디스패치 테이블로 에이전트의 종류를 찾아 충돌체 둘을 에이전트의 인자를 넘겨 호출하는 것이다.

 

12.3.5.7 움직이는 물체 간의 충돌 검출

작고 빠른 물체는 프레임 사이에 다른 물체를 통과할 수 있는 문제가 있다. (터널링 문제)

 

해결 방법

1. 스윕 형상

ㄴ 궤적 형상을 이용하는 것 (구 -> 캡슐)

ㄴ 충돌체가 곡선을 그리며 움직이는 경우는 문제가 생긴다.

ㄴ 충돌체가 회전하는 경우에 스윕 형상을 만들면 볼록하지 않을 수 있어 선형 보외법을 사용하여 볼록하게 만들 수 있는데 이는 부정확하다.

 

2. 연속 충돌 검출 (CCD, Coutinuous Collision Detection)

ㄴ 주어진 시간 구간 내에서 움지깅는 두 물체 간의 가장 이른 충돌 시각을 찾는 것

ㄴ 두 충돌체의 이전 시간 단계(프레임 단위)와 현재 시간 단계의 위치와 방향을 선형 보간해서, 이전 시간 단계와 현재 시간 단계 사이에 있는 임의의 시각에서 충돌체의 변환을 대략적으로 계산할 수 있다.

그런 후 이 움직임 경로에서 가장 이른 TOI (time of impact) 를 검색한다.

 

12.3.6 성능 최적화

충돌 검출은 CPU를 많이 쓰는 작업이다.

ㄴ 두 형상이 교차하는지 알아내는데 필요한 계산 자체가 매우 복잡하다.

ㄴ 게임 월드에는 수많은 물체들이 있는 것이 보통인데, 교차 검사 횟수는 물체 수가 많아질수록 급격히 증가한다.

 

물체 n개가 있으면 단순한 방식에서는 모든 쌍을 검사하기 떄문에 알고리즘은 n의 제곱 복잡도를 갖는다. 

그렇기 때문에, 공간 해시, 공간 분할, 계층적 경계 볼륨 기법 등이 교차 검사 횟수를 줄이는데 사용된다.

 

12.3.6.1 시간적 일관성

시간적 일관성 (프레임 간 일관성) 기법은 충돌체가 완만한 속도로 움직이고 있다면 다음 프레임에서 충돌체의 위치와 방향을 예측할 수 있다는 점에서 사용한다. 

매 프레임마다 위치와 방향을 다시 계산하는 수고를 덜 수 있다.

하복의 충돌 에이전트(hkpCollisionAgent)는 여러 프레임에 걸쳐 지속적으로 쓰인다.

 

12.3.6.2 공간 분할

옥트리, 이진 공간 분할 트리, kd-트리 등의 다양한 계층 분할 방식들이 충돌 검출 최적화 과정에서 공간을 분할하는데 쓰이다.

 

12.3.6.3 넓은 단계, 중간 단계, 좁은 단계

하복에서는 매 시간 단계에서 검사해야 하는 충돌체들을 선별하는 데 세 가지 단계를 사용한다.

 

1. 대강의 AABB 테스트

2. 복합 형상들을 대강의 경계 볼륨으로 검사

3. 개별적인 충돌 단위들로 테스트

 

Sweep And Prune 알고리즘

ㄴ 주요 엔진(하복 Physx)들이 모두 넓은 단계 충돌 검출에서 사용하는 알고리즘이다.

ㄴ 충돌체들의 AABB에서 세 좌표축 방향으로 최댓값과 최솟값을 구한 후 정렬된 리스트를 탐색해 교차하는 AABB를 찾는 방식이다. Sweep And Prune 알고리즘은 프레임 간 일관성 기법을 이용해 정렬에 걸리는 복잡도를 O(n log n)에서 O(n)으로 줄일 수 있다.

 

12.3.7 충돌 질의

캐스트란 임의의 물체를 충돌 월드에 위치시킨 후 반직선 혹은 선분만큼 움직이는 동안 어떤 물체와 충돌하는지, 혹은 충돌하지 않았는지를 판별하는 것이다.

캐스트는 대상이 실제로 충돌 월드에 존재하지 않는다는 점에서 일반적인 충돌 검출 과정과는 다르다.

 

12.3.7.1 레이 캐스트

시작점과 끝점이 있는 선분을 충돌 월드의 충돌체들과 테스트한다.

선분은 점 하나와 델타 벡터 하나로 표현한다.

 

충돌 검출 시스템은 시작점에서 가장 가까운 접촉 정보를 리턴하는 경우도 있고 어떤 충돌 검출 시스템은 모든 충돌체들을 알려주기도 한다

 

12.3.7.2 형상 캐스트

구 캐스트 캡슐 캐스트 등 여러 종류를 사용할 수 있다.

시작점 + 델타 벡터 + 형상의 종류 + 크기 + 방향의 정보를 사용한다.

 

블록 형상을 캐스트할 때는 다음의 두 경우를 따져봐야 한다.

1. 캐스트할 형상이 다른 충돌체를 관통하거나 아니면 접촉하고 있어 시작점에서 움직일 수 없는 경우

ㄴ 시작 지점에서 충돌이 일어났는지 아닌지 충돌 시스템이 정보를 알려준다. 접촉 지점은 캐스트 형상의 안에 있을 수도 있고, 표면에 있을 수도 있다.

 

2. 캐스트할 형상이 시작점에서 다른 어떠한 충돌체와 교차하거나 만나지 않아서 움직일 수 있는 경우

ㄴ 다른 물체와 부딪힐 때까지 선분을 따라 이동할 수 있다. 

 

레이 캐스트와 마찬가지로 캐스트 형상의 최초의 접촉들(형상은 한번에 여러 충돌체와 교차할 수 있다) 만 알려주는 API 있는 반면, 가상의 경로를 계속 진행해 그동아느이 모든 접촉을 알려주는 API도 있다.

 

응용

ㄴ 가상 카메라가 게임 월드의 물체와 충돌하는 지를 알아낼 때 사용한다.

ㄴ 캐릭터의 움직임을 구현할 때, 캡슐 캐스트를 흔히 사용한다. 예를 들면 캐릭터를 평평하지 않은 지형에서 앞으로 밀 떄 구나 캡슐을 캐릭터 발 사이에 놓고 움직이는 방향으로 캐스트하고, 두 번째 캐스트에서 아래위로 움직여보고 지면에 항상 닿아 있게 보정한다.

 

12.3.7.3 팬텀

게임 월드 안의 특정한 공간(예를 들어 캐릭터의 일정한 반경)에 있는 모든 충돌체를 탐지하고 싶은 경우 사용한다.

이동거리 벡터 D가 0인 구 캐스트와 비슷하다. 

 

구 캐스트와는 달리 팬텀은 충돌 월드에 지속적으로 존재한다. 팬텀은 보이지 않고, 역학 시뮬레이션에 관여하지 않는 충돌체와 같은 의미이다.

 

12.3.7.4 기타 충돌 질의

하복에는 Closed Point 질의가 있는데, 해당 충돌체에서 가장 가까운 다른 충돌체 위의 점을 찾는데 쓰인다.

 

12.3.8 충돌 필터링

게임에는 종종 특정한 종류의 물체 사이에 충돌을 켜고 끌 수 있는 기능이 필요하다. 예를 들면 대부분의 물체는 물 표면을 뚫고 지나갈 수 있다. 부력을 시뮬레이션해서 물에 뜨게 만들거나 바닥에 가라앉게 할 수도 있지만 어쨌건 수면이 단단해서는 안 된다. 거의 모든 충돌 엔진에서는 게임 나름의 기준에 따라 충돌체 간의 접촉을 허용하거나 허용하지 않을 수 있는 기능이 있다. 이를 충돌 필터링이라고 한다.

 

12.3.8.1 충돌 마스킹과 계층

게임 월드의 물체들을 범주로 나눈 후 테이블을 이용해 특정 범주의 물체가 다른 물체와 충돌할 수 있는지 없는지를 찾는 방법이다.

 

12.3.8.2 충돌 콜백

충돌이 검출될 때마다 콜백 함수를 호출하는 방법

하복 기준

접촉 지점이 처음 월드에 더해지면 contactPointAdded() 콜백을 호출

접촉 지점이 이후에 올바른 것으로 판별되면(이보다 이른 TOI가 발견되지 않으면) contactPointConfirmed() 콜백을 호출

이 콜백 함수들 안에서 충돌하는 대상에 따라 접촉 지점을 거부할 수 있다(필터링 할 수 있다).

 

12.3.8.3 충돌 머터리얼

충돌 했을 때 나는 소리나 효과 등의 부가적인 효과를 제어하기 위해 게임 월드의 충돌체들을 분류해야 할 필요가 있다.

충돌 기본 단위와 충돌 머터리얼을 연결하는데 흔히 쓰이는 방법은 8, 16, 32 비트 정수를 사용하는 것인데, 이 값으로 상세한 충돌 속성이 저장된 자료구조 배열에 접근한다.

 

12.4 강체 역학

게임 물리 엔진에서는 게임 월드 안에 있는 강체들의 움직임에 대해 여러 가지 제약 조건을 줄 수 있다.

가장 널리 쓰이는 제약 조건은 통과할 수 없는 조건이다.

대부분의 물리 시스템들은 시뮬레이션 하는 강체 간에 사실적인 상호작용 구현을 위해 개발자가 별도의 제약조건을 설정할 수 있게 지원하기도 한다.

경첩, 직선축 관절, 볼 관절, 바퀴, 래그 돌 등이 있다.

 

역학 시뮬레이션의 강체와 충돌 엔진의 충돌체 간에는 대개 일대일 관계가 있다. 하복의 hkpRigidBody 객체는 오직 한 개의 hkpCollidable 참조만 갖고 있다.

12.4.1 기본 지식

12.4.1.1 단위

강체 역학 시뮬레이션은 대부분 MKS 단위계를 이용한다. 미터(m), 킬로그램(kg), 초(s)를 사용한다.

 

12.4.1.2 선 동역학과 각 동역학의 구분

제약 없는 강체는 직교 좌표계의 세 축 방향으로 평행 이동하면서 마찬가지로 세 축에 대해 자유롭게 회전할 수 있는 물체다. 이런 경우 물체는 6자유도를 갖는다고 한다.

 

제약 없는 강체의 운동은 다음과 같이 서로 완전히 독립된 두 부분으로 나눌 수 있다.

 

선 동역학

ㄴ 회전 성분을 무시할 때 물체의 운동

 

각 동역학

ㄴ 물체의 회전 운동을 나타낸다.

 

질량 중심

ㄴ 선 동역학의 관점에서 제약 없는 강체는 모든 질량이 질량 중심이라 불리는 한 점에 있는 것처럼 행동한다.

ㄴ 강체의 질량은 질량 중심에서 모든 방향으로 균등하게 배분된다.

ㄴ 밀도가 균일한 물체의 경우 질량 중심은 입체의 기하 중심과 같다.

 

12.4.2 선 동역학

선 동역학의 관점에서는 어떤 강체의 위치를 위치 벡터로 표현할 수 있는데, 이 벡터는 월드 공간 원점에서 그 강체의 질량 중심을 가리킨다.

 

 

12.4.2.1 선속도와 선 가속도

강체의 선속도란 이 물체의 질량 중심이 움직이는 속도와 방향으로 결정된다. 벡터로 표현한다.

벡터 값으로 일반적으로 초당 미터 (m/s) 단위로 나타낸다.

시간에 대한 위치의 일차 도함수는 속도, 이차 도함수는 가속도이다.

 

12.4.2.2 힘과 운동량

질량이 있는 물체를 가속하거나 감속하는 모든 것을 힘이라고 한다.

크기와 뱡향이 있기 때문에 벡터 값(F)으로 표현한다.

 

F = ma 이므로 힘은 kg-m/s² 단위(뉴턴)이다.

 

12.4.3 운동 방정식 풀기

강체 역학의 핵심 문제는 작용하는 힘이 주어졌을 때 물체의 움직임을 구하는 것이다.

선 동역학의 경우 순수 힘 F(t)나 이전 시간의 물체 위치와 속도 등의 정보를 가지고  v(t)와 r(t)를 찾는 것을 의미한다.

 

12.4.3.1 함수로서의 힘

힘은 상수인 경우도 있지만 시간에 대한 함수인 경우도 있다.

또 힘은 물체의 위치나 속도 등 다른 값에 대한 함수가 될 수도 있다.

 

12.4.3.2 상 미분 방정식 (ODE, Ordinary Differential Equation)

상 미분 방정식이란 하나의 독립 변수 (t)에 대한 함수 하나와 그 도함수들로 이루어진 방정식을 일컨는 말이다.

12.4.3.3 분석적 해법

중력과 같이 가속도가 고정인 경우(힘이 일정한 경우) 에는 가능하지만 게임 물리에서는 분석적인 해법을 쓸 수 있는 경우가 드물다. 게임은 상호적인 시뮬레이션이기 때문에 힘이 어떻게 변할지 예측할 수 없다.

 

12.4.4 수치 적분

현재 시각에서 물체의 위치와 속도를 알고, 힘이 시간, 위치, 속도의 함수라면 이걸들을 이용해 다음 시각에서의 물체의 위치와 속도를 찾는 것이 목표이다.

 

12.4.4.1 명시적 오일러 법

모든 시간 단계마다 물체의 속도가 상수라는 가정을 하여 현재의 속도를 사용하여 다음 프레임에서 물체의 위치를 예측한다.

시간에 따라 속도가 일정할 때만 정확하다.

 

12.4.4.2 수치 해석적 방법의 특성

상 미분 방정식(ODE)에 대한 수치 해석적인 방법은 다음과 같은 속성이 있다.

 

수렴성

ㄴ 시간 단계 t가 0으로 수렴함에 따라 근사적인 해법은 점점 진짜 해법에 가까워지는가?

 

차수

ㄴ 오차는 대새 시간 단계 Δt의 몇 제곱에 비례하는가? 오차가 O(Δt²)라면 차수 1 이라고 한다.

ㄴ 명시적 오일러 법은 차수가 1인 방식이다. (테일러 급수에서 Δt의 1 제곱인 항까지 정확하기 때문)

ㄴ 차수가 클수록 오차가 작아진다.

ㄴ 오차는 대개 ODE를 테일러 급수 전개한 것과 ODE를 빼서 남아있는 항들의 차수를 이용해 구한다.

테일러 급수

 

안정성

ㄴ수치 해석적인 해법이 시간이 지남에 따라 안정화되는가?  

 

12.4.4.3 명시적 오일러 법 이외의 방식

명시적 오일러 법은 단순한 구현에 많이 쓰이며, 속도가 일정한 경우에는 잘 적용될 수 있다.

범용 역학 시뮬레이션에서는 오차가 크고 안정성이 낮기 때문에 쓰지 못한다.

 

여러 수치 해석적인 방법들이 ODE를 푸는데 쓰인다.

ㄴ 후향 오일러 법 (차수가 1인 방식)

ㄴ 중점 오일러 법 (차수가 2인 방식)

ㄴ 여러 종류의 Runge-Jutta 방식 

 

12.4.4.4 베를레 적분

오늘날 상호적인 게임에서 가장 많이 쓰이는 수치 해석적 ODE 해법이다.

 

일반 베를레 방식과 속도 베를레 방식이 있다.

 

일반 베를레 방식

ㄴ 오류 차수가 높다.(오차가 작음)

ㄴ 계산하기 복잡하지 않다.

ㄴ 가속도가 주어졌을 때 한 번에 위치를 바로 구할 수 있다.

 

테일러 급수 전개를 두 개 더한다. 시간을 앞으로 보내는 식과 뒤로 보내는 식이다.

 

다음 시간 단계에서의 위치는 가속도와 이전 위치들( 현재 t1, 과거 t1-Δt )로 표현된다.

일계 도함수 항(속도)이 사라짐을 알 수 있다.

순수 힘에 관한 식으로 베를레 방식을 표현하면 다음과 같다.

속도는 여러가지 방법으로 근사치를 얻을 수는 있다.

 

12.4.4.5 속도 베를레 방식

속도 베를레 방식은 일반 베를레 방식보다 널리 쓰인다. 네 단계로 이루어져 있고 편의를 위해 시간 단계를 두 부분으로 나눈다.

ODE를 알고 있을 때 아래의 4단계를 거친다.

 

12.4.5 2차원 각 동역학

제약 없는 강체는 질량의 중심을 중심으로 회전 운동을 한다.

이 말은 물체 질량 중심의 선운동에 각운동을 더하면 물체의 모든 운동을 완전히 나타낼 수 있다는 뜻이다.

이처럼 물체에 힘이 가해졌을 때 회전 운동을 연구하는 분야를 각동역학(angular dynamics) 이라고 한다.

 

2차원에서는 각 동역학은 선 동역학과 완전히 똑같은 식으로 동작한다.

 

12.4.5.1 방향과 각속력

2차원에서는 모든 선 운동이 xy 평면에서만 일어나고 모든 회전 운동은 z축을 기준으로 발생한다.

2차원 강체의 방향은 각 θ만 갖고도 나타낼 수 있다. x축 양의 방향을 바라보고 있을 때를 θ = 0으로 정할 수 있다.

 

12.4.5.2 각속력과 가속도

각속도란 물체의 회전각이 시간에 따라 얼마나 빠르게 변하는지를 나타내는 값이다 

각속도는 스칼라 함수 ω(t)로 나타내고, 단위는 초당 라디안 (rad / s)이다.

각속도 (좌) / 선속도 (우)

각가속도 α(t)는 각속도의 변화율이며, 초의 제곱당 라디안 (rad / s²)이다.

각가속도 (좌) / 선가속도 (우)

 

12.4.5.3 관성 모멘트

점 질량의 선 속도를 바꾸기 힘든 정도를 나타내는 개념이 질량이라면, 어떤 강체를 특정한 축 기준으로 회전할 때 각속도를 바꾸기 힘든 정도를 나타내는 개념이 관성 모멘트이다.

 

물체의 질량이 회전축에 모여있는 경우에는 상대적으로 회전하기 쉽기 때문에 회전축에서 멀리 떨어져 있는 경우보다 관성 모멘트가 작다.

 

지금은 2차원 각운동만을 다루기 때문에 회전축은 항상 z축이며, 물체의 관성 모멘트는 그냥 스칼라 값이다. 관성 모멘트는 흔히 기호 I로 나타낸다.

 

12.4.5.4 토크

힘이 작용하는 경로가 물체의 질량 중심을 통과하는 경우는 지금까지 봐 왔던 선 운동만 일어난다. 그렇지 않은 경우 선 운동 외에도 토크라 불리는 회전하는 힘이 생긴다.

 

물체의 질량 중심에서 힘이 작용하는 지점까지의 벡터 r과 힘 F를 외적 연산 한 값이 토크 N이다

N = r X F

N = r X F

2차원에서 r과 F는 모두 xy 평면에 놓여있기 때문에 N은 언제나 z축 방향이다. 

 

토크 (좌) / 힘 (우)

 

12.4.6 3차원의 각 동역학

12.4.6.1 관성 텐서

3차원에서 강체의 회전 질량은 관성 텐서라 불리는 3x3 행렬로 나타낸다. 이것은 보통 기호 I로 표기한다.

이 행렬의 대각선 성분들은 좌표계의 세 축에 대한 관성 모멘트를 나타낸다. 대각선이 아닌 성분들은 관성 곱이라고 부른다. 이 값들은 물체가 세 좌표축에 모두 대칭일 때는 0이 된다.(육면체 박스처럼) 이 값들이 0이 아닐 경우 물리적으로는 사실적이지만 다소 직관적이지 않아 보이는 운동을 하게 되는데, 보통 게이머들은 이것을 사실적이지 않은 운동이라고 생각하기 쉽다. 그렇기 때문에 게임 엔진에서 쓰이는 관성 텐서는 종종 성분이 세 개인 벡터로 단순화하는 경우가 많다.

 

12.4.6.2 3차원에서의 방향

사원수를 물체의 방향을 나타내는 데 사용하고, 물체의 방향은 시간에 대한 함수이므로 q(t)로 써야 한다.

회전각이 0인 임의의 방향을 정해야 한다.

 

12.4.6.3 3차원에서의 각속도와 운동량

3차원에서 각속도는 벡터 값이고 ω(t)로 표기한다.

각속도 벡터는 회전축을 나타내는 단위 벡터 u에 그 축에 대한 물체의 2차원 각속도를 곱한 것으로 생각하면 된다.

3차원에서는 회전하는 강체에 작용하는 힘이 없더라도 각속도 벡터 ω(t)는 일정하지 않을 수 있는데, 이것은 회전축이 계속 변할 수 있기 때문이다.

 

각 속도는 보존되는 값이 아니기 때문에 역학 시뮬레이션에는 각 운동량 L을 주요 물리량으로 다룬다.

각 운동량 L(t) = Iω(t)

 

12.4.6.4 3차원에서의 토크

3차원에서도 토크를 구하는 방법은 힘의 작용점 벡터와 힘 벡터의 외적을 사용한다.

각속도는 보존되는 값이 아니기 때문에 각운동량을 기준으로 표현해야 한다.

 

운동량 = 질량 x 속도

각운동량 = 관성 모멘트 x 각속도

 

12.4.6.5 3차원 각운동 방정식 풀기

어려우니까 결론만

명시적 오일러 법을 사용했을 때 3차원 각운동 ODE의 근사 해

 

12.4.7 충돌 응답

물체가 서로 충돌하는 경우 역학 시뮬레이션에서는 물체드링 사실적으로 충돌에 반응하면서 시뮬레이션 단계가 끝나는 시점에서는 서로 뚫고 들어간 상태로 남아 있지 않게 조치를 취해야 한다. 이를 충돌 응답이라고 한다.

 

12.4.7.1 에너지

힘은 강체들의 계(system) 에 에너지를 더하거나 (폭발) 에너지를 감소시킨다. (마찰)

에너지는 위치 에너지와 운동 에너지로 구성된다.

운동 에너지

선 운동에서 발생하는 운동 에너지(선운동량 p와 속도 벡터 v)
회전 운동에서 발생하는 운동 에너지 (각운동량 L과 각속도 ω)

 

12.4.7.2 충격적 충돌 응답

대부분의 실시간 시뮬레이션들은 뉴턴의 법칙에 기반을 두고 운동량과 운동에너지를 분석한다.

 

이 법칙은 충돌의 속성을 단순화하는 다음과 같은 가정에 바탕을 둔다.

 

1. 충돌 힘은 무한히 짧은 시간에 작용하고, 따라서 이상적인 충격이라고 할 수 있다. 그렇기 때문에 물체의 속력은 충돌 결과 즉시 변한다.

 

2. 물체들이 접촉하는 지점의 표면에는 마찰이 없다. 이 말은 충돌 과정에서 두 물체를 떨어뜨리려는 충격은 접촉 표면에 수직으로 작용한다는 뜻이다. 즉 충돌 충격에는 비스듬한 성분이 없다.

 

3. 충돌 과정에서 일어나는 물체들의 분자 단위 반응들은 반발 계수(보통 ε로 표현한다.)라는 수 하나로 흉내 낼 수 있다. 이 수는 충돌로 인해 얼마만큼의 에너지를 잃는가를 나타낸다. ε = 1일 경우 완전 탄성 충돌이고 에너지를 잃지 않는다. ε = 0일 경우에는 비탄성 충돌이며 두 물체는 운동에너지를 완전히 잃는다.

 

완전 탄성 충돌의 경우 에너지 손실(T lost) = 0이고 완전 비탄성 충돌의 경우 첨자가 붙은 운동에너지의 합은 0이 된다.

 

12.4.7.3 페널티 힘

충돌 응답을 구현하는 또 다른 방식에는 실제로는 존재하지 않는 페널티 힘이라는 개념을 시뮬레이션에 도입하는 방법이 있다.

스프링 상수 K는 교차해 있는 시간을 효율적으로 조정하는 역할을 하고, 감쇠 계수 b는 반발 계수와 다소 비슷한 역할을 한다.

b = 0인 경우 감쇠 효과는 없다. (완전 탄성 충돌이다)

 

12.4.7.4 제약조건을 통한 충돌 해결

대부분의 물리 엔진에서 물체의 운동에 여러 제약 조건을 넣어 시뮬레이션 할 수 있다.

제약조건으로 충돌을 처리한다면 그냥 시뮬레이션의 제약조건 해결사(constraint solver)를 실행해 해결하면 된다. 제약조건 해결사가 빠르고 품질 높은 시각적 결과를 뽑아낸다면 이 또한 효율적인 충돌 처리법이 된다.

 

12.4.7.5 마찰

12.4.7.6 용접 (WELDING)

물체가 다각형 수프를 따라 미끄러지고 있는 경우 추가적으로 문제가 발생한다.

물체가 한 삼각형에서 같은 다각형 수프의 다른 삼각형으로 미끄러지는 경우 충돌 검출 시스템은 이 물체가 다음 삼각형의 모서리에 닿을 것을 예측해 불필요한 충돌을 감지하게 된다.

 

하복 4.5에서는 메시에 삼각형 인접 정보를 첨가하는 방식을 사용한다.

이 방식을 사용하면 충돌 검출 시스템은 어떤 모서리가 어느 모서리가 다각형 수프 내에 있는 모서리인지 알수 있기 때문에 가짜 충돌 정보를 정확하고 빠르게 버릴 수 있다. 하복은 이것을 용접이라고 부르는데, 사실상 다각형 수프의 삼각형들의 모서리를 용접한 것과 마찬가지이기 때문이다.

 

12.4.7.7 점차 멈춰 서기, 섬, 잠재우기

운동하던 물체가 마찰, 감쇠 등을 통해 에너지를 잃으면 움직이던 물체들은 점차 멈춰야 한다. 하지만 컴퓨터 시뮬레이션을 통해 점차 멈춰서는 일은 부동소수 오차나 반발력 계산 오류, 수치적인 불안정성 등의 여러 요인 때문에 물체들이 자연스럽게 멈춰 서지 못하고 계속 불안정하게 떨리게 된다.

 

시스템에서 물체의 에너지를 제거할 수도 있고, 물체의 속도가 일정한 기준 아래로 내려가면 그냥 속도를 0으로 만들 수도 있다.

 

물체가 움직이지 않게 되면 더 이상 매 프레임 물체의 운동 방정식을 계산할 필요가 없기 때문에 매 프레임 계산 대상에서 제외할 수 있다. (잠재우기) 잠자는 물체들은 시뮬레이션에서는 잠시 제외되지만 충돌에서는 여전히 유효하며, 잠자는 물체에 힘이나 충격이 작용하거나 물체를 평행 상태로 지탱하고 있던 접촉 지점을 잃게 되면 깨어나게 되고 다시 동적 시뮬레이션을 시작한다.

 

잠재우는 기준(평행 상태 판단 기준)

ㄴ 물체가 3개 이상의 점촉 지점(한 개 이상의 접촉 평면)을 통해 중력 등 기타 작용하는 힘과 평형을 이루고 있는 상태

ㄴ 물체의 선 운동량과 각 운동량이 미리 정해진 기준보다 낮을 때

ㄴ 물체의 선 운동량과 각 운동량의 이동 평균이 정해진 기준보다 낮을 때

ㄴ 물체의 총 운동 에너지 ( T = 1/2p v + 1/2L ω)가 정해진 기준보다 낮을 때

 

시뮬레이션 섬

하복과 PhysX에서는 한층 더 성능을 최적화하기 위해 서로 영향을 주거나 곧 영향 줄 가능성이 있는 물체들을 자동으로 한데 묶어 시뮬레이션 섬이라고 지칭한다.

각 시뮬레이션 섬들은 서로 독립적으로 시뮬레이션되기 때문에 캐시 일관성이나 병렬 처리에 유리한 방식이다.

잠재우기를 시뮬레이션 섬 단위로 한다.

 

12.4.8 제약조건

제약 없는 강체는 세 방향으로 평행 이동할 수 있고, 세 직교 좌표축을 기준으로 회전할 수 있어서 자유도가 6이다.

제약 조건은 강체의 자유도를 부분적으로 감소시키거나 없앨 수 있다.

물리 SDK에서는 다양한 제약조건을 지원한다.

 

12.4.8.1 점과 점 제약조건

물체 A의 한 점이 물체 B의 한 점에 항상 붙어 있어야 한다.

 

12.4.8.2 단단한 스프링

물체 A의 한 점은 물체 B의 한 점과 일정한 거리만큼 떨어져 있어야 한다.

 

12.4.8.3 경첩 제약조건

회전 운동의 자유도를 1로 제약해 경첩의 축 방향으로만 회전할 수 있도록 한다.

 

12.4.8.4 각기둥 제약조건

평행 운동의 자유도를 1로 제약해 축 방향으로만 평행이동 할 수 있도록 한다.

 

12.4.8.5 흔히 쓰이는 다른 제약조건

평면, 바퀴, 도르래

 

12.4.8.6 제약조건 체인

여러 물체가 제약조건들로 인해 체인 형태로 연결된 경우 시뮬레이션하기 어려울 때가 있는데, 제약 조건 체인이라는 제약조건들의 특수한 그룹을 사용하여 제약 조건 해결사에 물체들이 어떻게 연결되어 있는지에 대한 정보를 제공해야 한다.

 

12.4.8.7 래그 돌

제약조건 체인을 이용한다.

래그 돌은 물리 시스템에 의해 좌우되는 (물리월드의 강체들의 위치와 회전으로 애니메이션 뼈대의 위치와 방향을 정한다.) 절차적 애니메이션 (실행 시간에 생성하는 애니메이션)이다.

 

12.4.9 강체의 움직임 제어

대부분의 게임에서는 디자인적 요소로 중력의 영향을 받아 움직이거나 여러 물체들 사이의 충돌에 의해 자연스럽게 운동하는 강체들의 움직임을 어느 정도 통제할 수 있어야 한다.

 

12.4.9.1 중력

중력은 힘이 아니라 가속도이기 때문에 질량에 관계없이 모든 물체에 똑같은 영향을 미친다.

모든 곳에서 쓰이면서 특수한 성질이 있기 때문에 SDK의 전역 설정으로 지정하는 경우가 많다.

 

12.4.9.2 힘 가하기

게임에서 물리 시뮬레이션을 하는 동안 물체들에는 수많은 힘이 가해질 수 있다. 힘은 한정된 시간에 걸쳐 작용한다.(즉시 작용하는 경우 충격이라고 한다.)

게임 안에서 힘은 특성상 대개 동적이다.(프레임마다 바뀐다.) 그렇기 때문에 거의 모든 물리 SDK에서는 힘을 적용하는 함수들을 프레임마다 한 번씩, 힘이 유효한 동안(여러 프레임) 계속 호출하도록 설계한다.

 

12.4.9.3 토크 가하기

질량 중심에서 등거리인 두 지점에 크기가 같고 방향이 정반대인 힘을 가하면 순수하게 토크만 생긴다.

이처럼 쌍으로 작용해 토크를 만드는 두 힘을 짝힘이라고 한다.

 

12.4.9.4 충격 가하기

충격은 즉각적인 운동량의 변화다.

엄밀한 의미에서 충격은 무한히 짧은 시간동안 작용하는 힘이다. 시간 간격을 통해 역학 시뮬레이션을 구현하는 경우 가장 짧은 힘 적용 시간은 Δt인데 충격을 제대로 흉내내기에는 너무 긴 시간이다. 대개의 물리 엔진은 applyImpulse 같은 함수로 물체에 충격을 가하게 된다.

 

12.4.10 충돌/물리 단계

충돌/물리 엔진의 업데이트 단계에서 일어나는 일

1. 물리 월드의 물체들에 힘과 토크를 Δt 만큼 앞선 시각으로 계산하여 다음 프레임에서의 위치와 방향을 시험적으로 결정한다.

 

2. 시험적인 이동으로 인해 물체들 간에 새로운 접촉 정보가 생겼는지를 알아내기 위해 충돌 검출 라이브러리를 호출한다.(시간적 일관성을 활용하기 위해 물체들은 각각의 접촉 정보를 유지한다. 따라서 시뮬레이션하는 매 단계 충돌 엔진은 기존의 접촉 정보가 유효하지 않게 바뀌었느지와 새로운 접촉 정보가 더해졌는지만 알아내면 된다)

 

3. 충돌을 해결한다. 충격을 가하거나 페널티 포스를 가하는 방법, 또는 제약조건 해결 과정에서 처리하는 방법 등이 쓰인다. SDK에 따라서는 이 단계에 충돌 검출을 수행할 수도 있다.

 

4. 제약조건 해결사에 의해 제약조건을 충족시키게 한다.

 

 

12.4.10.1 제약조건 해결사

제약조건 해결사는 반복적 알고리즘이다.

물체의 실제 위치 및 회전을 제약조건이 충족될 떄의 이상적인 위치 및 회전과 최대한 근접하게 함으로써 다수의 제약조건을 동시에 만족시키는 것이 목적이다. 따라서 제약 조건 해결사는 반복적인 오차 최소화 알고리즘이라 할 수 있다.

 

물리 시뮬레이션의 매 단계에서는 수치 적분을 이용해 물체들의 변환을 예측한다. 그런 후 제약조건 해결사는 물체들의 상대적인 위치를 가지고 두 물체가 공유하는 회전축이 얼마나 틀어졌는지를 계산한다. 오차가 발견되면 해결사는 물체들을 움직여 오차를 최소화하거나 아니면 없애려 시도한다.

오차가 없다면 더 이상 반복할 필요가 없다.

 

여러 개의 제약조건을 동시에 충족해야 하는 경우에는 여러 번 반복해야 할 수도 있다.

오차를 최소화할 수 있게 잘 설계된 제약조건 해결사가 있지만 그렇지 않은 경우에는 물체들이 엉뚱하게 움직이는 것을 볼 수도 있다.

 

12.4.10.2 엔진 간의 구현 차이

엔진마다 여러 계산 단계를 수행하는 방법이나 상대적인 수행 순서는 물리 SDK마다 차이가 있을 수 있다.

 

12.5 물리 엔진과 게임 통합

12.5.1 게임 객체와 강체간의 연결

게임 객체는 충돌/물리 월드에 여러 개의 강체로 표현되기도 하고 강체가 아예 없을 수도 있다.

 

1. 강체가 없는 경우

ㄴ 장식으로 쓰이는 물체들이나, 볼 수는 있지만 근처에 갈 수 없는 물체로 쓰인다.

 

2. 강체가 하나인 경우

ㄴ 대부분의 단순한 게임 객체는 강체 하나로 표현할 수 있다. 이 경우 강체의 충돌체는 가능한 한 게임 객체의 모양과 비슷한 것을 고른다.

 

3. 강체가 여러 개인 경우

ㄴ 캐릭터, 탈것 등 여러 견고한 부분들로 구성된 물체들. 이런 게임 객체들은 대개 뼈대를 이용해 각 부분의 위치를 관리한다.

 

게임 객체와 강체 간의 연결은 엔진에서 관리하며, 보통 각 게임 객체가 자신의 강체들을 관리하는 경우가 많다.

 

12.5.1.1 물리 주도 물체

게임에 강체 역학 시스템을 사용하는 경우 완전히 시뮬레이션에 의해서만 움직이는 물체들을 물리 주도 물체들이라고 한다.

 

우선 시뮬레이션을 처리하고 그후 물리 시스템에서 물체의 위치와 방향을 얻어 온다. 이 변환 정보는 게임 객체에 그대로 적용되거나 아니면 일부 관절, 혹은 그 안의 자료 구조 등에 적용된다.

 

12.5.1.2 게임 주도 물체

게임 물체들 중 일부는 물리에 영향 받지 않고 움직여야 한다. 이런 물체들은 충돌 검출에는 참여해야 하지만(에를 들면 물리 주도 물체들을 밀쳐내며 이동하는 경우), 물리 엔진에 의해 움직임에 영향을 받아서는 안 된다. 이런 기능을 위해 대부분의 물리 SDK는 게임 주도 물체라 불리는 특수한 강체를 지원한다. (하복에서는 key framed body라고 부른다).

 

게임 주도 물체들은 시뮬레이션 하는 동안 힘과 충돌 충격에 의해서 속도가 바뀌지 않아야(움직이지 않아야) 하기 때문에 무한대의 질량을 가진다고 설정한다.

 

게임 주도 물체들을 물리 월드에서 움직이렴녀 단순히 매 프레임마다 게임 객체의 자리를 따라 위치와 방향을 바꿔서는 안 된다. 이렇게 하면 물리 시뮬레이션에서 해결하기 굉장히 어려운 불연속적인 요소를 집어넣게 된다. 

 

그렇기 때문에 게임 주도 물체들은 대개 충격을 이용해 다음 프레임에 물체가 원하는 위치에 오게 속도에 즉각적인 변화를 준다. 대부분의 물리 SDK는 다음 프레임에서 원하는 위치와 방향에 도달하는데 필요한 충격의 성분과 각 성분을 계산해주는 함수를 제공한다.

 

12.5.1.3 고정된 물체

게임 월드는 대개 정적인 형상과 동적인 객체들이 섞여 이루어진다. 게임 월드의 정적인 부분을 모델링하기 위해 대부분의 물리 SDK에는 고정된 물체라 불리는 특수한 종류의 강체가 있다. 고정된 물체는 게임 주도 물체와 비슷한 면이 있지만 동적인 시뮬레이션에는 전혀 관여하지 않는다. 이것들은 충돌만 하는 물체들이다.

 

12.5.1.4 하복의 모션 타입

하복에서는 hkpRigidBody 클래스의 인스턴스를 이용해 모든 종류의 강체들을 나타낸다. 각 인스턴스에는 모션 타입을 지정하는 부분이 있다. 모션 타입은 물체가 고정돼 있는지, 아니면 게임 주도인지 물리 주도인지를 시스템에 알려주는 역할을 한다.

 

하복은 물체의 종류를 모션타입만을 바꿈으로써 쉽게 바꿀 수 있다.(고정된 물체, 게임 주도 물체, 물리 주도 물체)

 

12.5.2 시뮬레이션 업데이트

물체 시뮬레이션을 완전히 업데이트 하려면 다음과 같은 단계를 거쳐야 한다. 업데이트는 대개 프레임당 한 번씩 한다.

 

게임 주도 강체 업데이트

ㄴ 물리 월드에 있는 게임 주도 강체들의 변환을 업데이트해서 게임 월드에 있는 쌍들의 변환과 일치되게 한다.

 

팬텀 업데이트

ㄴ 팬텀 형상은 게임 주도 충돌체처럼 동작하지만 강체는 없다. 이는 여러 종류의 충돌 질의를 하는 데 쓰인다. 팬텀의 위치는 물리 단계 전에 업데이트해 충돌 검출이 수행될 때 제 위치에 자리하게 한다.

 

힘 업데이트, 충격 적용, 제약조건 조정

ㄴ 게임에서 가해지는 모든 힘들을 적용한다.

ㄴ 해당 프레임에서 게임 이벤트에 의해 발생한 모든 충격을 적용한다.

ㄴ 필요한 경우 제약조건을 조정한다.

 

시뮬레이션 단계 밟기

ㄴ 운동 방정식을 수치 적분하는 과정

ㄴ 충돌 검출

ㄴ 충돌 해결

ㄴ 제약조건 적용하기 

 

물리 주도 게임 객체 업데이트

ㄴ 물리 월드에서 물체들의 변환을 뽑아 낸 후 해당하는 게임 객체나 관절의 변환을 업데이트한다.

 

팬텀 질의

ㄴ 물리 단계가 끝난 후, 각 팬텀 형상의 접촉 정보를 읽어와 다양한 의사 결정에 이용한다.

 

충돌 캐스트 질의 수행

ㄴ 레이 캐스트와 형상 캐스트를 수행하는데, 이 과정은 동기화되는 경우도 있고 비동기화 되는 경우도 있다. 얻은 결과는 게임 엔진에서 다양한 의사 결정에 쓰인다.

 

업데이트 의존 관계

ㄴ 게임 주도 물체 업데이트와 힘/충격 업데이트는 시뮬레이션 단계보다 앞에 수행해야 한다.

ㄴ 물리 주도 게임 객체 업데이트는 시뮬레이션 단계가 끝난 후에 수행되어야 한다.

ㄴ 레이 캐스트와 형상 캐스트는 게임 루프 안에서 아무 때나 할 수 있다.

ㄴ 렌더링은 게임 루프의 마지막에 온다.

 

12.5.2.1 충돌 질의 시점 정하기

충돌 시스템에서 최신 정보를 얻어 오려면 해당 프레임의 물리 단계(시뮬레이션 단계)가 끝난 후 충돌 질의를 던져야 한다. 하지만 물리 단계는 대개 프레임의 맨 마지막에 수행되는데 이때는 이미 게임 로직에서 대부분의 의사 결정이 끝난 상태이고 게임 주도 물체들의 새 위치도 정해진 상태이다. 

 

충돌 질의를 언제 던져야 하는지에 대한 정답은 없지만 2가지 선택 방법이 있다.

1. 이전 프레임의 상태를 바탕으로 정하기

ㄴ 이전 프레임의 충돌 정보를 바탕으로 해도 정확한 의사 결정을 할 수 있는 경우가 많기 때문에 사용 가능하다.

ㄴ 물체가 너무 빠르게 이동하면 오류가 생길 수 있다.

 

2. 물리 단계 이후에 충돌 질의하기

ㄴ 충돌 질의 결과를 바탕으로 한 의사 결정이 맨 마지막까지 미뤄져도 상관 없는 경우 이 방법을 쓸 수 있다. 충돌 질의에 따라 달라지는 렌더링 효과가 그 좋은 예다.

 

12.5.3 게임에서 충돌과 물리를 사용하는 예

나중에 필요할 때 찾아보기

 

12.5.3.1 단순한 강체를 갖는 게임 객체

12.5.3.2 총알 추적

12.5.3.3 수류탄

12.5.3.4 폭발

12.5.3.5 파괴 가능 물체

12.5.3.6 캐릭터 구현

12.5.3.7 카메라 충돌

12.5.3.8 래그 돌 통합

 

12.6 고급 물리 기능

강체 시뮬레이션을 넘어서 물리엔진이 확장되는 분야들

 

가변 형상 물체

ㄴ 하드웨어가 발달하고 고성능 알고리즘이 개발되면서 물리 엔진에서도 가변 형상 물체를 지원하기 시작했다.

 

의복

ㄴ 의복은 스프링으로 연결된 점 질량들이 종이 형태로 배열된 모양으로 모델링할 수 있다. 하자만 의복은 다른 물체와의 충돌이나 시뮬레이션에서 수치적인 안정성 등 많은 부분에서 문제가 발생할 수 있기 때문에 구현하기 까다롭다.

 

머리카락

ㄴ 수많은 가는 섬유 형태로 모델링하거나 밧줄 형태나 가변 형상 물체로 모델링할 수 있다.

 

물 표면 시뮬레이션과 부력

일반 유체 역학 시뮬레이션

 

물리 기반 오디오 합성

ㄴ 물리적으로 시뮬레이션되는 물체가 좀 더 사실적으로 보이게 하기 위해서 적절한 오디오를 동적으로 합성해 생성하는 방법

 

GPGPU

ㄴ GPU의 병렬 처리 능력을 그래픽이 아닌 다른 일에 사용

 

11.1 캐릭터 애니메이션의 종류

11.1.1 셀 애니메이션

셀 애니메이션을 전자적인 형태로 구현한 것이 스프라이트 애니메이션이다.

셀 애니메이션은 2D 게임 시대의 핵심 기법이었다.

 

11.1.2 계층적 강체 애니메이션

초기의 3D 캐릭터 애니메이션 기법은 캐릭터를 여러 조각의 강체로 모델링하는 계층적인 강체 애니메이션이었다.

캐릭터 애니메이션에 사용한다면 몸체가 관절에서 갈라지기 때문에 보기에 좋지 않다.

 

11.1.3 정점 애니메이션과 모프 타겟

정점 애니메이션 기법을 사용하면 애니메이터가 만든 메시의 정점들로부터 게임 엔진에서 실시간으로 움직일 정점 데이터를 추출할 수 있다. 하지만 이 방법을 사용하면 시간에 따라 변화하는 움직임 정보가 메시의 각 정점에 저장되어야 하기 때문에 데이터 양이 매우 커져 실시간 게임에는 잘 사용되지 않는다.

 

실시간 게임에 사용되는 기법에서는 모프 타겟 애니메이션 기법을 사용한다.

이 방식에서는 애니메이터가 메시의 정점들을 움직여 상대적으로 적은 개수의 고정된 핵심 포즈를 몇 개 만든다.

실행 시에는 이 고정된 핵심 포즈를 두개 혹은 그 이상을 섞어 애니메이션을 생성할 수 있다. 생성되는 애니메이션의 각 정점 위치는 핵심 포즈들의 정점 위치로부터 선형 보간 기법을 사용하여 계산할 수 있다.

 

모프 타겟 기법은 주로 표정 애니메이션에 사용된다. 사람 얼굴은 50개 이상의 근육으로 이루어진 굉장히 복잡한 조직이기 때문이다.

 

11.1.4 스킨 애니메이션

정점 애니메이션이나 모프 타겟 애니메이션과 같이 애니메이션되는 메시의 삼각형들을 변형할 수 있다는 장점이 있다.

그러면서도 효율적인 성능과 메모리 사용량을 갖는 계층적 강체 애니메이션의 이점도 함께 갖는다.

 

뼈대의 관절부분에 스킨이라 불리는 매끄럽고 연속적인 삼각형 메시가 붙는다.

스킨의 정점들은 관절의 움직임을 따라간다.

스킨 메시의 각 정점들은 다수 관절에 가중치를 갖고 결합될 수 있어 관절의 움직임에 따라 자연스럽게 스킨이 늘어날 수 있게 된다.

 

11.2 뼈대

뼈대는 관절이라고 하는 강체 조각들의 계층으로 이루어져 있다.

 

11.2.1 뼈대 계층 구조

뼈대의 관절들은 계층 구조나 트리 구조로 형성된다. 하나의 루트 관절이 있고 다른 모든 관절들은 자식이 되거나 자식의 자식이 되는 식으로 연결된다.

 

보통 각 관절에 0에서 N-1까지 번호를 붙인다. 각 관절마다 부모 관절의 번호를 저장하도록 하면 계층 구조 전체를 저장할 수 있다.

 

11.2.2 뼈대를 메모리에 저장

일반적인 뼈대의 자료구조

struct Joint
{
    Matrix4x3 m_invBindPose; // 관절의 바인드 포즈 역변환
    
    const char* m_name; // 관절의 이름
    
    U8 m_iParent; // 부모 관절의 인덱스 // 8비트로 최대 256개의 관절을 지원 // 루트라면 -1 (0xFF)을 저장
    
}

struct Skelton
{
    U32 m_jointCount; // 관절의 수
    
    Joint* m_aJoint; // 관절의 배열
}

 

11.3 포즈

11.3.1 바인드 포즈 (= 레퍼런스 포즈, 레스트 포즈, T 포즈)

3D 메시가 뼈대에 연결되기 전의 포즈이다.

뼈대가 연결되지 않은 삼각형만으로 이루어진 원래 상태의 메시 모습이다.

11.3.2 로컬 포즈

관절의 포즈는 대부분의 경우 부모 관절을 기준으로 지정한다.

관절의 포즈는 이동, 회전, 크기 변환의 합성행렬로 나타낼 수 있다.

전체 뼈대의 포즈는 모든 관절의 포즈의 집합이라고 할 수 있다.

 

11.3.2.1 관절 스케일

포즈나 애니메이션에서 스케일을 생략하거나 제한하면 몇 가지 이득이 있다.

관절 스케일(크기 변환)을 사용하지 않으면 저장 공간을 줄일 수 있다. (크기 행렬 파라미터를 받지 않아도 된다.)

비균일 스케일을 허용하면 관절의 경계 구가 타원체로 변화하지 않는다. 그렇기 때문에 절두체 검사나 충돌 검사를 관절 단위로 하는 엔진에서 필요한 연산을 대폭 줄일 수 있다.

 

11.3.2.2. 관절 포즈를 메모리에 저장

관절 포즈는 대개 SQT 형태로 저장된다. 

struct JointPose
{
    Quaternion m_rot; // Q
    Vector3 m_trans; // T
    F32 m_scale; // S
}

struct SkeletonPose
{
    Skeleton* m_pSkeleton; // 뼈대 자료구조, 관절의 개수 정보
    JointPose* m_aLocalPose;  // 각 관절의 로컬 관절 포즈
}

 

11.3.2.3 기저 변환으로서의 관절 포즈

관절 j의 좌표 공간을 표현한 점 혹은 벡터에 관절 표준 변환(QTS로 만든 변환)을 적용한 결과는 부모 공간에서의 점이나 벡터가 된다.

 

11.3.3 글로벌 포즈 (= 모델 공간 포즈)

관절 j의 모델 공간 포즈를 얻으려면 그 관절에서 시작해 뼈대의 계층을 루트까지 거슬러 올라가면서 각 관절의 로컬 포즈를 곱해 나가면 된다.

 

11.3.3.1 글로벌 포즈 구현

매번 행렬의 곱셈을 계산하기보다는 SkeletonPose 구조체에 글로벌 포즈 변환 행렬을 멤버로 추가하는 방법을 사용한다.

 

11.4 클립

게임은 캐릭터가 어떻게 움직이고 행동할지 예측할 수 없기 때문에 게임 애니메이션은 길게 이어지는 프레임으로 제작하지 않는다.대신 게임 캐릭터의 움직임을 수 많은 세세한 동작 단위로 끊어서 만든다. 이 개별적인 움직임들을 애니메이션 클립 또는 그냥 애니메이션이라고 부른다.

 

각 클립에는 캐릭터가 수행하는 잘 정의되는 동작 한 개가 들어 있다.

어떤 클립은 걷거나 뛰기 사이클처럼 루핑되는 것도 있고 한 번씩만 재생되는 것도 있다.

 

애니메이션을 클립을 사용하지 않고 제작하는 경우는 플레이어와 캐릭터가 상호작용을 하지 않는 부분에 등장하는 IGC, NIS, FMV 장면을 만드는 경우이다.

 

인 게임 시네마틱(IGC, in-game cinematic), 비상호작용 장면(NIS, non-interactive sequence)는 일반적으로 게임 엔진에서 실시간으로 렌더링 하는 경우를 지칭한다.

 

풀 모션 비디오(FMV, full-motion video)는 어떤 장면을 미리 MP4나 WMV 또는 다른 형식의 동영상 파일로 만들어 놓은 다음 게임 엔진의 전체 화면 동영상 재생기로 실행 시 재생하는 경우를 말한다.

 

11.4.1 로컬 타임라인

모든 애니메이션 클립은 로컬 타임라인을 갖고 있다. 보통 독립된 변수 t로 표현하고,

 t의 값은 0 (클립의 시작점) ~ T(클립의 전체 재생시간) 범위의 값을 가진다.

 

11.4.1.1 포즈 보간과 연속 시간

애니메이터가 프레임 단위로 포즈를 잡을 필요는 없다.

애니메이터는 클립 내의 특정 시각에 키 포즈나 키 프레임이라 불리는 주요한 포즈를 만든다.

비어 있는 클립은 컴퓨터를 이용, 선형이나 곡면 보간 등의 방법을 사용하여 채운다.

포즈들 사이를 보간하는 엔진의 기능 덕분에 정수 프레임 뿐만 아니라 실수 프레임에서의 캐릭터 포즈도 추출할 수 있다.

시간 척도 변수를 사용하여 애니메이션을 원래보다 빠르게, 느리게, 거꾸로 재생할 수 있다. 실수 프레임에서의 포즈 추출이 이럴 때 필요하다.

 

11.4.1.3 프레임과 샘플

프레임이라는 용어는 1/30초 1/60초와 같이 시간 간격을 나타낼 때 쓰고

애니메이션의 특정 시각을 나타낼 때는 샘플이라는 용어를 쓰자.

 

11.4.1.4 프레임, 샘플, 루핑 클립

루핑 클립에서 첫 번째 포즈와 마지막 포즈는 똑같기 때문에 보통 게임 엔진은 루핑 클립의 마지막 샘플을 생략한다.

 

루핑 클립이 아닌 경우 N 프레임 애니메이션에는 N + 1 개의 고유한 샘플이 있다.

루핑 클립인 경우 마지막 샘플은 군더더기이므로 N 개의 고유한 샘플이 있다.

 

11.4.1.5 정규화된 시간(위상)

타임라인 변수의 범위를 0 ~ T에서 0 ~ 1로 정규화 하면 편리한 경우가 있다.

재생시간이 2초(60프레임)인 달리기 애니메이션을 3초(90프레임)인 걷기 애니메이션으로 부드럽게 크로스 페이드 하고 싶 은 경우

11.4.2 글로벌 타임라인

게임의 모든 캐릭터에는 글로벌 타임라인(캐릭터가 처음 게임 월드에 생성된 시점, 특정 레벨이 시작되는 시점 또는 게임이 처음 시작할 때 시작되는 시계)이 있다.

 

애니메이션을 재생한다라는 것을 클립의 로컬 타임라인을 캐릭터의 글로벌 타임라인에 매핑한다고 생각할 수 있다.

 

애니메이션 클립을 글로벌 타임라인에 매핑하려면 다음과 같은 클립의 정보를 알고 있어야 한다.

ㄴ 글로벌 시작 시간

ㄴ 재생 비율 R 

ㄴ 총 재생 시간 T

ㄴ 반복 재생해야 할 횟수 N

11.4.3 로컬 클록과 글로벌 클록 비교

애니메이션 시스템은 현재 재생중인 모든 애니메이션의 시간 인덱스를 관리하고 있어야 한다.

여기에는 두 가지 방법이 있다.

 

로컬 클록

ㄴ 각 클립은 고유한 로컬 클록을 갖고 있다. (초 단위, 프레임 단위, 또는 정규화된 시간 단위)

 

글로벌 클록

ㄴ 캐릭터는 대개 초로 측정되는 글로벌 클록을 갖고, 각 클립은 재생을 시작한 시점의 글로벌 시각을 저장하기만 하면 된다. 클립의 로컬 클록을 이 정보들을 이용하여 계산한다.

 

글로벌 클록 방식을 사용하면 한 캐릭터를 동기화하는 것뿐만 아니라 여러 캐릭터의 동기도 쉽게 맞출 수 있다.

예를 들어 캐릭터가 주먹으로 때리는 애니메이션과 NPC의 맞는 애니메이션을 동기화 하는 경우 로컬 클록의 방식에서는 두 애니메이션 사이에 프레임 지연이 발생할 가능성이 있다.

11.4.4 간단한 애니메이션 데이터 형식

struct AnimationSample
{
    JointPose* m_aJointPose; // 애니메이션의 샘플 한 개는 뼈대 내 각 관절들의 모든 포즈로 이루어진다.
};

struct AnimationClip
{
    Skeleton* m_pSkeleton; // 뼈대 자료구조
    F32 m_framesPerSecond; // 초당 프레임 수
    U32 m_frameCount; // T(총 재생 시간, 전체 샘플 수)
    AnimationSample* m_aSamples; // 샘플의 배열
    bool m_isLooping; // 루프 클립인지 아닌지
}

실제 게임 엔진에서는 애니메이션을 이러한 형식 그대로 저장하지는 않는다. 보통은 저장 공간을 줄이려고 여러 방식으로 애니메이션 데이터를 압축한다.

 

11.4.4.1 애니메이션 리타겟팅

애니메이션은 한 가지 뼈대에만 적합하지만, 비슷한 구조로 이루어진 뼈대들의 경우에는 예외다. 예를 들어 기본 계층구조에 영향을 미치지 않는 부가적인 말단 관절만 다르고 전체 뼈대의 구조는 동일한 경우에는 애니메이션을 공유할 수 있다.

이렇게 하려면 현재 애니메이션 되는 뼈대에는 없는 관절인데 이것의 채널 데이터가 있다면 그냥 무시하도록 엔진을 만들어야 한다.

 

11.4.5 연속 채널 함수

애니메이션 클립의 샘플은 시간에 대한 연속함수로 정의할 수 있다.

대다수 게임 엔진은 샘플을 선형적으로 보간하는데, 이 경우 사용되는 함수는 원래의 연속함수를 구분적 선형근사(piece-wise linear approximation)한 것이다.

 

11.4.6 메타 채널

많은 게임에서는 애니메이션에 필요한 부가적인 데이터를 사용할 수 있도록 메타 채널을 허용한다.

메타 채널에는 뼈대의 포즈를 잡는 일에는 직접 영향을 미치지는 않지만 애니메이션과 동기화해야 하는 게임에 특화된 정보를 담을 수 있다.

흔히 다양한 시간 인덱스(샘플)에서의 이벤트 트리거를 저장하는 특별한 채널을 정의한다.

애니메이션의 로컬 시간 값이 트리거를 지날 때 마다 이벤트를 게임 엔진으로 보내 필요한 처리를 할 수 있도록 한다.

예를 들어 애니메이션 특정 시점에서의 소리나 파티클 효과를 재생하는데 사용한다.

 

11.5 스키닝과 행렬 팔레트 생성

스키닝 : 3D 메시의 정점들을 포즈를 잡은 뼈대에 연결 하는 과정

 

11.5.1 정점별 스키닝 정보

skinned mesh는 정점에 의하여 뼈대에 연결된다. 각 정점은 관절 한 개이상의 관절에 묶일 수 있다.

관절 한 개에만 묶인 정점은 관절의 움직임과 똑같이 움직인다. (붙어있다.)

두 개 이상의 관절에 묶인 정점은  각 관절을 따라 따로 움직인 경우의 위치를 구한 후, 각 위치를 가중치에 따라 가중 평균하여 최종 위치를 결정한다.

 

3D 아티스트가 메시의 각 정점에 다음과 같은 정보를 지정해야 한다.

ㄴ 정점이 묶일 관절의 인덱스

ㄴ 정점과 묶인 관절들의 가중치

 

보통 하나의 정점에 묶일 수 있는 관절의 수는 4개를 최대로 잡는다. 관절 인덱스 표현을 8비트씩 4개로 32비트 워드 하나에 들어가도록 하면 좋기 때문에

 

가중치 4개의 합은 1이기 때문에 정점에는 3개의 가중치만 저장한다.

 

11.5.2 스키닝 수학

메시의 정점을 바인드 포즈에 맞는 위치에서 현재 뼈대의 포즈에 맞는 위치로 변환해 줄 행렬이 필요한데 이를 스키닝 행렬이라고 부른다.

 

스킨드 메시의 정점도 모델 공간에서 정의된다.

 

모든 관절마다 스키닝 행렬을 계산해야 한다. (= 스키닝 행렬의 배열 = 행렬 팔레트)

 

애니메이션 엔진이 각 관절의 스키닝 행렬을 계산하는 순서

1. 각 관절의 로컬 포즈를 계산

2. 로컬 포즈를 글로벌 포즈로 바꾸기

3. 캐시된 관절의 바인드 포즈의 역행렬과 곱하기

 

11.5.2.3 모델-월드 변환 통합

모든 정점은 언젠가는 모델 공간에서 월드 공간으로 변환되어야 한다. 따라서 어떤 게임 엔진은 스키닝 행렬 팔레트에 물체의 모델 월드 변환을 먼저 곱해 놓는다.

 

한 애니메이션을 동시에 여러 캐릭터에 적용해야 한다면 모델-월드 변환을 통합하면 안된다.

 

11.5.2.4 다관절에 묶인 정점의 스키닝

정점에 스키닝 행렬을 곱하고 가중치를 곱한 값들의 합을 새로운 정점으로 사용한다.

 

11.6 애니메이션 블렌딩

특정한 시각에서 두 개 이상의 포즈를 합쳐 같은 시각에서의 출력(하나의 포즈)을 만든다.

짧은 시간에 걸쳐 소스 애니메이션에서 목적 애니메이션으로 부드럽게 전환하는데 일시적으로 블렌딩을 사용할 수 있다.

11.6.1 LERP 블렌딩

LERP 연산을 SQT의 각 성분에 개별적으로 적용한다.

 

T는 벡터 LERP

Q는 사원수 LERP나 SLERP

S는 벡터 LERP

 

두 뼈대 포즈를 선형 보간할 때 가장 자연스로운 중간 포즈는 관절의 부모 공간(로컬 공간)에서 각 관절 포즈를 독립적으로 보간할 때 나온다. 포즈 블렌딩은 로컬 공간에서 하기 때문에 각 관절 포즈의 선형 보간은 뼈대의 다른 관절들에 완전히 독립적이다. 즉 포즈를 선형 보간하는 일은 멀티프로세서 구조에서 완전히 병렬로 수행할 수 있다는 뜻이다.

 

11.6.2 LERP 블렌딩 적용

11.6.2.1 시간 블렌딩

샘플된 포즈들 사이의 중간 포즈를 구할 때 사용한다.

t = 2, t = 3에서의 샘플된 포즈만 알고 있을 때, 만약 t= 2.18에서의 샘플을 얻고 싶다면 t = 2와 t = 3 에서의 포즈를 가중치 비율 0.18로 보간하면 된다.

 

11.6.2.2 움직임 연속성, 크로스 페이딩

클립 전환 중에도 캐릭터 신체 각 부분이 완벽하게 부드럽게 움직이도록 하는 것이 목표이다. (정점들의 위치가 연속적으로 바뀌어야 한다. = C0 연속성)

1차 도함수 또한 연속적이어야 한다.(C1 연속성)

 

LERP 기반 애니메이션 블렌딩을 사용할 경우 보기 좋은 C0 연속성을 구현할 수 있고 C1 연속성에 근접한 효과를 낼 수도 있다.

 

클립 사이의 전환에 LERP 블렌딩을 활용하는 것을 크로스 페이딩이라고 부르기도 한다.

 

크로스 페이딩의 종류

부드러운 전환

ㄴ 동기화가 잘 되어 있어야 한다.

ㄴ 블렌딩 계수를 0부터 1까지 증가시키며 두 클립을 동시에 재생한다.

 

동결 전환

ㄴ 이전 클립의 로컬 클록은 이후 클립이 재생을 시작하는 순간 멈춘다.

ㄴ 두 클립이 별로 연관성이 없고 시간을 동기화할 수 없는 경우에도 잘 동작한다.

 

블렌딩 계수를 일차원 베지어와 같은 시간의 3차 함수로 만들면 더 부드럽게 전환하게 만들 수도 있다.

 

11.6.2.3 방향 이동

타겟 이동 (targeted movement)

ㄴ 애니메이터가 전진 이동, 왼쪽 스트레이핑, 오른쪽 스트레이핑 세 종류의 루핑 애니메이션을 만들어야 한다.

ㄴ 이동 방향에 따라 인접한 클립을 섞는다. (전진, 왼쪽)을 섞거나 (전진, 오른쪽)을 섞는다. 이동 방향의 각도가 두 인접한 클립에 얼마나 가까운지에 따라 블랜드 비율을 정한다.

 

선회축 이동 (pivotal movement)

ㄴ 캐릭터의 수직축을 기준으로 캐릭터를 회전하면서 그냥 앞으로 이동하는 루프 애니메이션을 재생

11.6.3 복합 LERP 블렌딩

11.6.3.1 일반적인 1차원 LERP 블렌딩

1차원 범위 안에 다수의 애니메이션 클립과 블렌딩 계수를 새로 정의하여 블렌딩 계수와 인접한 두 클립을 블렌딩한다.

 

11.6.3.2 단순한 2차원 LERP 블렌딩

2차원 범위 에서 인접한 4개의 클립을 블렌딩한다.

 

11.6.3.3 삼각형 2차원 LERP 블렌딩

2차원 블렌딩 벡터 b가 주어지면 2차원 블렌딩 공간에서 세 클립으로 형성되는 삼각형의 무게중심 좌표를 계산함으로써 

3개의 블렌딩 가중치 값을 구할 수 있다. 3개의 클립을 블렌딩한다.

 

11.6.3.4 일반적인 2차원 LERP 블렌딩

2차원 블렌딩 공간에 임의로 배치도니 임의 갯수 애니메이션 클립에도 무게 중심을 구하는 방법을 확장할 수 있다.

들로네 삼각분할 기법을 이용해 임의의 점들로 이루어진 삼각형들을 구하는 방법이다.

 

11.6.4 부분-뼈대 블렌딩

사람은 신체 각 부분을 따로 움직일 수 있다. 예를 들면 걸으면서 오른손을 흔들고 왼손으로는 뭔가 가리킬 수 있다.

부분 뼈대 블렌딩 이라고 부르는 기법을 이용하면 게임에서 이와 같은 움직임을 구현할 수 있다.

 

각 관절마다 서로 다른 블렌드 비율을 쓸 수 있도록 하면 된다.

블렌드 마스크를 사용하여 특정한 관절의 블렌드 비율을 0으로 설정하여 특정 관절에 애니메이션이 적용되지 않게 할 수 있다.

 

부분-뼈대 블렌딩은 캐릭터 움직임이 부자연스러운 문제가 있다.

ㄴ 관절별 블렌드 인자가 급격하게 변하면 신체의 일부분이 나머지 부분과 따로 움직이는 것처럼 보인다.

ㄴ 사람의 신체는 절대 독립적으로 움직이지 않기 때문에 부자연스럽다.

11.6.5 가산 블렌딩

가산 블렌딩은 두 애니메이션 클립의 차이를 나타내는 차이 클립을 만든다.

한 번 차이 클립을 만들면 원래의 참조 클립 뿐만 아니라 전혀 상관없는 다른 클립들에도 더할 수 있다.

 

예를 들어 (지친 상태로 뛰는 애니메이션 - 평범하게 달리는 애니메이션)을 걷기 애니메이션에 더하면

지친상태로 걷는 애니메이션이 된다.

 

11.6.5.1 수학 공식

D = S(소스 클립) - R(참조 클립) 인데 관절 포즈는 자기 관절의 공간을 부모 관절의 공간으로 변환 하는 행렬이다.

행렬 연산에서 - 는 역행렬의 곱셈이므로 관절의 차이 포즈는

소스 포즈 행렬에 참조 포즈의 역행렬을 곱한 것이다.

 

입력 클립 S와 R의 재생시간이 같을 때만 차이 클립을 구할 수 있다.

 

11.6.5.3 가산 블렌딩의 한계

기존 애니메이션에 움직임을 더하는 방식을 쓰기 떄문에 뼈대의 관절을 과도하게 회전시키는 경향이 있다.

문제를 피하기 위한 팁

ㄴ 참조 클립의 엉덩이 관절 회전을 최소화한다.

ㄴ 참조 클립의 어깨와 팔꿈치 관절을 중립 위치에 두어 팔이 과도하게 돌아가는 것을 방지한다.

ㄴ 애니메이터는 각 코어 포즈마다 새로운 차이 클립을 만들어야 한다.

11.6.6 가산 블렌딩 활용

ㄴ 발 자세 변이

ㄴ 움직임 소음

ㄴ조준과 바라보기

ㄴ시간 축의 재해석

 

11.7 후처리

마야와 같은 애니메이션 도구를 이용해 export한 데이터를 이용한 것이 아니라 실행 시간에 생성하는 애니메이션을 절차적 애니메이션이라고 한다.

 

수작업으로 만든 애니메이션 클립에서 뼈대의 포즈를 먼저 잡고, 절차적 애니메이션으로 포즈를 수정하는 후처리 작업을 한다.

11.7.1 절차적 애니메이션

원래의 애니메이션의 포즈를 후처리하기 위해서 변화가 필요한 관절의 Q, S, T 채널을 변경시켜 최종 포즈를 결정하게 한다.

11.7.2 역운동학 (IK, inverse kinematics)

캐릭터가 물체를 집어 드는 애니메이션 클립에서 캐릭터가 물체를 잡도록 뼈대의 최종 포즈를 조정해 목표하는 물체와 일치시키고자 할 때 사용하는 기법

 

역운동학에서는 한 관절의 글로벌 포즈(end effector)가 원하는 위치로 움직일 수 있도록 다른 관절들의 로컬 포즈를 변경하는 것이 목표이다.

 

11.7.3 래그 돌

캐릭터가 죽었을 때 몸이 축 늘어진다. 이러한 상황에서는 몸이 주변 환경에 따라 물리적으로 현실감 있게 반응하여야 한다. 이런 목적으로 래그 돌을 사용한다.

 

11.8 압축 기법

한 개의 관절 포즈에 부동소수점 수 10개가 필요할 수 있다(평행이동 3개, 회전변환 4개, 스케일변환 3개)

뼈대 관절이 100개이고, 30프레임으로 샘플된 1초 길이의 클립이라면

10 * 4바이트 * 100 * 30 =  12000 바이트 

1초당 117KB의 메모리를 사용한다.

 

11.8.1 채널 생략

애니메이션 클립의 크기를 줄이는 가장 단순한 방법은 관계없는 채널을 생략하는 것이다.

 

대부분의 캐릭터는 불균등 스케일을 사용할 필요가 없기 때문에 스케일 채널을 3개에서 1개로 줄일 수 있다.

캐릭터의 뼈들은 일반적으로 늘어나지 않기 때문에 루트 관절이나 얼굴 관절, 떄로는 쇄골뼈의 관절을 제외하고는 평행이동도 생략할 수 있다. (3개 -> 0개)

사원수는 언제나 정규화되기 때문에 4개의 성분중 3개만 저장하고 마지막 성분은 실행 시에 계산할 수 있다. (4개 -> 3개)

 

전체 재생시간 동안 포즈가 변하지 않는 관절의 채널은 t=0에서만 기록하고 한 비트 값으로 남은 시간동안 변하지 않는다는 것을 표시하는 방법으로 채널을 생략할 수 있다.

 

11.8.2 양자화

각 채널의 크기를 32비트 부동소수가 아닌 16비트 정수로 바꿔 표현하는 방법

부동소수점 수 값을 정수로 바꾸는 과정을 인코딩

정수를 부동소수점 수 값으로 바꾸는 과정을 디코딩

 

11.8.3 샘플링 주기와 키 생략

애니메이션 데이터가 큰 이유

1. 각 관절에 10개의 부동소수점 수를 갖는 채널이 있어서

2. 뼈대에 관절이 많아서

3. 애니메이션 샘플링 빈도가 높아서 (30프레임)

 

1번 이유는 채널 생략으로 극복할 수 있다.

2번 이유는 고품질 캐릭터의 관절 갯수를 줄이기는 힘들기 때문에 해결하기 힘들고

3번 이유는

ㄴ 샘플링 빈도 낮추기 : 30프레임에서 15프레임으로 낮추면 애니메이션 데이터를 절반으로 줄일 수 있다.

 

ㄴ 샘플 생략하기 : 클립에서 일정 시간 동안 채널 데이터가 선형에 가까운 변화를 보인다면 이 기간 동안의 샘플은 양 끝점들을 제외하고 모두 생략할 수 있다. 샐행 시에 선형 보간을 하여 생략된 샘플들을 복원한다.

 

11.8.4 커브 기반 압축

애니메이션을 일정 시간 간격의 포즈 샘플이 아니라 각 관절의 S, Q, T 채널들의 시간에 따른 궤적을 n차원, 불균등, 비유리수 B-스플라인의 집합으로 기록하는 방법이다.

B-스플라인을 사용하면 많은 굴곡을 갖는 채널들을 몇 개의 데이터로 인코딩할 수 있다.

 

11.8.5 선택적 로딩과 스트리밍

애니메이션이 특정 레벨에서만 쓰인다면 그 레벨이 로딩될 때 같이 로드하고 레벨이 끝날 때 해제하도록 하면 메모리를 잘 관리할 수 있다.

 

11.9 애니메이션 시스템 아키텍처

대부분의 애니메이션 시스템들은 최대 세 개의 개별 레이어들로 구성된다.

 

애니메이션 파이프라인

ㄴ 게임에서 애니메이팅되는 캐릭터와 객체에 대하여 하나 이상의 애니메이션 클립들과 블렌드 적용 값을 입력으로 블렌딩하여 하나의 로컬 뼈대 포즈를 출력한다.

ㄴ 각 관절의 스키닝 행렬, 글로벌 뼈대 포즈를 계산하여 출력한다.

 

액션 상태 머신

ㄴ 게임 캐릭터의 동작들은 유한 상태 머신으로 모델링하는 것이 제일 좋은 방법이다.

ㄴ 동시에 여러 동작을 할 때, 캐릭터 몸체의 각 부위들이 독립적으로 동시에 움직일 수 있도록 해준다.

 

애니메이션 컨트롤러들

ㄴ 특정 상황에 처한 캐릭터의 행동을 관리하는 시스템

 

11.10 애니메이션 파이프라인

1. 클립 압축 해제와 포즈 추출

ㄴ 각 입력 클립에 대한 로컬 뼈대 포즈를 출력한다.

ㄴ 이 포즈가 가지는 정보는 뼈대에 있는 모든 관절에 대한 정보이거나, 관절의 일부분에 대한 정보이거나, 첨가 블렌딩에서 사용되는 차이 포즈가 될 수 있다.

 

2. 포즈 블렌딩

ㄴ 하나 이상의 애니메이션 클립을 블렌딩할 때만 실행되는 단계, 뼈대에 있는 모든 관절들에 대한 하나의 로컬 포즈를 출력한다.

 

3. 글로벌 포즈 생성

ㄴ 뼈대에 대한 계층구조를 따라서 지역 관절들을 조합하여 뼈대에 대한 글로벌 포즈를 생성한다.

 

4. 후처리

ㄴ 포즈를 마무리 하기 전에 뼈대의 로컬/글로벌 포즈를 수정한다.

ㄴ 후처리 단계에는 IK, ragdoll physics 기법을 적용할 수 있다.

 

5. 글로벌 포즈가 최종적으로 만들어지고 난 후 각 관절들의 글로벌 포즈 행렬을 그것과 일치하는 바인드 포즈 역행렬과 곱하여 렌더링 엔진 입력에 알맞는 스키닝 행렬 팔레트(배열)을 출력으로 생성한다.

 

11.10.1 자료 구조

11.10.1.1 공유 자원 데이터

모든 게임 엔진 시스템들은 공유 자원 데이터와 인스턴스별 상태 정보를 명확하게 구별해야 한다.

 

일반적으로 같은 유형의 캐릭터나 객체들은 하나의 자원 데이터를 공유하여 사용한다.

ㄴ 뼈대 - 뼈대는 관절과 관절 계층 구조를 묶어서 만든 포즈를 나타낸다.

ㄴ 스킨 메시 - 하나의 뼈대에는 여러 개의 스킨 메시들이 붙을 수 있다.

ㄴ 애니메이션 클립 - 한 캐릭터의 뼈대에 많은 애니메이션 클립이 적용될 수 있다.

 

게임에서 사용하는 고유한 뼈대들의 수를 최소한으로 줄이는 것이 도움이 된다.

 

11.10.1.2 인스턴스별 데이터

특정 캐릭터 형태에 대한 인스턴스 정보

1. 클립 상태

ㄴ 로컬 클록, 재생 비율

 

2. 블렌드 명세

ㄴ 블렌드 노드 트리 방식 : 블렌드 트리를 공유 자원으로 처리한다.

ㄴ 가중 평균 방식 : 블렌드 가중치를 인스턴스 별 상태 정보의 일부분으로 저장한다.

 

3. 부분 뼈대 관절 가중치

ㄴ 부분 뼈대를 블렌딩하게 되면 각 관절이 마지막 포즈에 미치는 영향 정도를 관절 가중치로 지정한다.

 

4. 로컬 포즈, 글로벌 포즈, 매트릭스 팔레트

 

11.10.2 균일 가중 평균 블렌딩 표현

모든 활성 애니메이션 클립(가중치가 0이 아닌 클립)들은 리스트로 관리된다.

N개의 활성 애니메이션들에서 평행 이동벡터들, 회전 사원수, 스케일 값들을 추출하여 가중평균을 계산한다. 이렇게 해서 뼈대의 마지막 포즈를 만든다.

 

11.10.3 블렌드 트리 (일반적으로 많이 씀)

애니메이션 블렌드 트리의 내부 노드들은 연산자이고 말단 노드들은 연산자에 제공되는 입력을 나타낸다.

ㄴ 11.6.3 복합 Lerp 블렌딩에서 다룬 내용들이다.

 

11.10.4 크로스-페이딩 아키텍처

크로스 페이드는 에니메이션 엔진이 균일 가중 평균을 사용하는지 또는 수식 트리 구조(블렌드 트리)를  사용하는지에 따라 두 가지로 구현될 수 있다.

 

11.10.4.1 균일 가중 평균 방식을 이용한 크로스-페이드

클립들의 가중치들을 조정하여 크로스-페이드를 구현한다.

복합 블렌드에서 다른 복합 블렌드로 전환하는 것을 크로스 페이딩으로 처리하는 것이 약간 까다롭다.

ㄴ 클립 그룹의 가중치의 합이 항상 1이도록 유지해야한다. 복합 블렌드를 전환하는 경우 가중치의 값이 0과 1 사이일 때 각 그룹의 가중치들을 더해도 1이 되지 않을 수 있기 때문에 그룹이 어떻게 묶여 있는지를 저장하는 메타 데이터가 더 필요하게 된다.

 

11.10.4.2 수식 트리를 이용한 크로스-페이드

 

복합 블렌드에서 다른 블렌드로 전환하든지 클립에서 클립으로 전환하든지 동일한 방식을 사용한다.(직관적이다.)

 

11.10.5 애니메이션 파이프라인 최적화

하드웨어에 따라 고유한 최적화 문제가 있다.

캐시 미스와 로드-적중-저장 연산을 피해야 한다.

부동소수점 연산을 피해야한다.

 

11.11 액션 상태 머신

액션 상태 머신은 애니메이션 파이프라인 위에 위치하며, 게임에서 캐릭터의 액션들을 상태 기반으로 직접 조정할 수 있도록 해준다.

11.11.1 애니메이션 상태

액션 상태 머신에서 각 상태는 동시에 처리되는 애니메이션 클립들에 대한 임의의 복합 블렌드에 해당된다. 

 

특정 애니메이션 상태에 해당하는 블렌드 트리는 단순하거나 복잡할 수 있다 예를 들어 대기 상태는 몸 전체 애니메이션 하나에 적용될 수 있고, 달리기 상태는 왼쪽 스트레이핑, 전방달리기, 오른쪽 스트레이핑을 각각 -90도 0도 90도로 섞는 반원형 블렌드에 해당된다.

11.11.2 상태 전환

 

11.11.2.1 상태 전환의 종류

소스 상태의 마지막 포즈와 목적 상태의 첫 번째 포즈가 정확하게 일치하다면, 한 상태에서 다른 상태로 바로 전환시킬 수 있다. 그렇지 않으면 크로스 - 페이드 방식으로 전환해야 한다.

크로스-페이드 방식이 부자연스러운 경우 한 상태에서 다른 상태로 변할 때만 사용할 전환 상태라는 특수한 상태를 상태 머신에 구현한다.

 

11.11.2.2 상태 전환 파라미터들

2가지 상태들 사이의 특정 전환을 표현할 때 조정할 수 있는 파라미터

ㄴ 소스와 목적지 상태들

ㄴ 전환 타입 (바로 전환인지, 크로스-페이드 인지, 전환 상태를 통해 실행 하는지)

ㄴ 기간 (크로스 페이드가 얼마 동안 일어나야 하는지)

ㄴ ease-in / ease-out 곡선 타입

 

11.11.2.3 상태 전환 행렬

n개의 상태를 가지는 상태 머신에서 가질 수 있는 전환의 개수는 최대 n의 제곱개 이다.

2차원 정방행렬을 사용하여 세로 축에 있는 상태에서 가로 축에 있는 다른 상태로 전환 가능한 모든 경우를 나타낼 수 있다.

 

11.11.3 상태 레이어들

상태 머신은 한 순간에 하나의 상태만 가질 수 있기 때문에 몸에서 

상태 계층을 사용하여 문제를 해결 할 수 있다.

n개의 각 계층에서 블렌드 트리를 계산하고 n개의 골격 포즈를 생성하고 블렌딩함으로써 뼈대의 최종 포즈를 계산한다.

11.11.4 컨트롤 파라미터들

블렌드 가중치, 재생 비율, 컨트롤 파라타미터를 조정하는 것은 쉬운 일이 아니다.

블렌드 가중치는 블렌딩에서 서로 다른 효과를 낸다. 어떤 가중치는 움직임 방향을 조정할 수 있고, 어떤 가중치는 캐릭터의 움직임 속도를 조정할 수도 있다. 따라서 상위 코드에서 블랜드 가중치들에 접근하는 방법이 있어야 한다.

 

노드 검색

ㄴ 상위 레벨의 코드에서 트리에 있는 블렌드 노드를 찾는 방법을 제공한다.

 

이름을 가진 변수

ㄴ 각 컨트롤 파라미터에 이름을 부여하고, 코드는 컨트롤 파라미터를 이름으로 찾아서 원하는 값으로 조정할 수 있다.

 

컨트롤 구조체

ㄴ 부동 소수점이나 구조체의 배열같은 간단한 자료 구조로 전체 캐릭터에 대한 컨트롤 파라미터들을 모두 저장하여 관리하고 블렌드 트리의 노드는 배열의 인덱스를 통해 찾는다.

'읽은 책 > 게임 엔진 아키텍처' 카테고리의 다른 글

14. 게임플레이 시스템의 소개  (0) 2022.08.17
12. 충돌과 강체 역학  (0) 2022.08.17
10. 렌더링 엔진  (0) 2022.08.12
9. 디버깅과 개발 도구  (0) 2022.08.10
8. 휴먼 인터페이스 장치 (HID)  (0) 2022.08.09

 

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)

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

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

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

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

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

+ Recent posts