Post Lists

레이블이 [Programming] Game인 게시물을 표시합니다. 모든 게시물 표시
레이블이 [Programming] Game인 게시물을 표시합니다. 모든 게시물 표시

2019년 12월 14일 토요일

Practical SIMD Programming from Jacco Bikker

http://www.cs.uu.nl/docs/vakken/magr/2017-2018/files/SIMD%20Tutorial.pdf
Practical SIMD Programming

Introduction
현대 CPUs들은 점점 더 최상의 성능을 얻기 위해 병렬성에 의존한다. 가장 잘 알려진 형태는 task parallelism인데, 그것은 여러개의 코어들, 하이퍼쓰레딩, 그리고 multitasking operating system을 지원하는 dedicated instructions에 의해서 하드웨어 수준으로 지원된다. instruction level parallelism이라고 알려진 병렬성은 덜 알려져있다 : 여러개의 명령어들을 동시에 실행하는 CPU의 능력이다. 즉, 같은 사이클에서, 한 단일 쓰레드에서 이다. 오리지널 펜티엄 같은 오래된 CPUs들은 high-latency floating point operations에 대해 병렬로 두 개의 파이프라인들을 활용하여 명령어들을 수행하기 위해 이것을 사용했다. 일반적으로, 이것은 프로그래머에게도 분명하게 발생한다. 최근의 CPUs들은 instruction level parallelism의 급격하게 다른 형태를 사용한다. 이러한 CPUs들은 vector operations의 다용도의 집합을 이용한다 : 4개 또는 8개의 입력에 대해 작동하는 명령어들, 4개 또는 8개의 결과를 만들어내면서, 하나의 cycle안에. 이것은 SIMD로 알려져있다: Single Instruction, Multiple Data. 이 compute potential를 이용하기 위해, 우리는 더 이상 컴파일러에게 의존할 수 없다. 광범위한 데이터 병렬성을 보여주는 알고리즘들은 explicit SIMD programming으로부터 가장 이득을 얻는다. 4배 ~ 8배 그리고 그 이상으로 잠재적 성능 향상이 있다. 이 문서는 C++과 C#에서 SIMD programming에 대한 실용적인 입문을 제공한다.

SIMD Concepts
CPU는 연산할 데이터를 저장할 registers를 사용한다. 일반적인 레지스터는 32 또는 64bits를 저장하고 단일의 scalar value를 보유한다. CPU 명령어들은 일반적으로 두 개의 operands에 대해서 작동한다. 다음의 코드를 고려해라:


vec3 velocity = GetPlayerSpeed();
float length = velocity.Lenght();

vector의 길이를 계산하는 line은 많은 scalar operations을 요구한다:


x2 = velocity.x * velocity.x
y2 = velocity.y * velocity.y
z2 = velocity.z * velocity.z
sum = x2 + y2
sum = sum + z2
length = sqrtf(sum)

Vector registers는 4개 (SSE) 또는 8개 (AVX) scalars를 저장한다. 이것은 C# 또는 C++ vector가 assembler level의 vector로 남아있게 된다는 것을 의미한다 : 세 개의 registers에 세 개의 별개의 값들을 저장하기 보다, 우리는 single vector register에 4개의 값들 (x, y, z 그리고 dummy value)를 저장한다. 그리고 x, y, 그리고 z를 별개로 제곱하는 것이 아니라, 우리는 세 개의 값을 (뿐만 아니라 dummy value도) 곱하는 단일의 SIMD 명령어를 사용한다.

이 간단한 예제는  SIMD code를 다룰 때 우리가 다룰 필요가 있는 많은 문제들을 보여준다:

  • 세 개의 컴포넌트를 가진 벡터에 대해 연산을 할 때, 우리는 vector processor의 완전한 연산 잠재성을 사용하지 않는다 : 우리는 SIMD register에서 'slots'의 25% (SSE) 또는 62.5% (AVX)를 낭비한다.
  • vector register에서 세 개의 scalars를 저장하는 것은 무료가 아니다 : 그 비용은 우리가 나중에 논의할 많은 요소들에 의존한다. 이것은 계산에 어떤 오버헤드를 더한다.
  • 마지막 줄의 square root는 여전히 단일 값에 대해 수행된다. 그래서, 비록 이것은 가장 비싼 line일지라도, 그것은 vector hardware로부터 이익을 얻지 못하고 우리의 이득을 제한한다.
이러한 걱정들을 완화하는 믿을만한 방법이 있다. 우리의 어플리케잇녀이 실제로 4명의 플레이어가 있는 게임이라고 가정하자:


for (int i = 0; i < 4; ++i)
{
    vec3 velocity = GetPlayerSpeed();
    float length = velocity.Length();
}

이 시나리오에서, 우리는 동시에 4개의 벡터들에 대해 연산할 수 있다:


x4 = GetPlayerXSpeeds();
y4 = GetPlayerYSpeeds();
z4 = GetPlayerZSpeeds();
x4squared = x4 * x4;
y4squared = y4 * y4;
z4squared = z4 * z4;
sum4 = x4sqaured + y4squared;
sum4 = sum4 + z4squared;
length4 = sqrtf4(sum4);

우리가 완전히 SIMD vectors로부터 C++/C# vector 개념을 분리시킨다는 것을 주목해라: 우리는 간단히 병렬로 원래의 스칼라 함수를 4번 실행하기 위해 SIMD vectors를 사용한다. 모든 라인은 이제 100% 효율성으로 SIMD instruction을 사용한다 (당연하게도, 우리는 AVX에 대해 8명의 플레이어들이 필요하다), 그리고 심지어 그 square root는 이제 4개의 숫자에 대해 계산된다.

여기에서 주의해야 할 한 가지 중요한 것이 있다: 처음 세 개의 lines을 효율적으로 만들기 위해, player speeds는 이미 `SIMD_friendly` format으로 저장되어야만 한다, 즉 : xxxx, yyyy, zzzz 으로. 이렇게 구성된 데이터는 직접적으로 vector register에 복사되어질 수 있다.

우리는 그 컴파일러가 우리를 위해 이것을 자동으로 하도록 기대할 수 없다. 효율적은 SIMD code는 효율적인 데이터 layout을 요구한다; 즉 이것은 수동으로 되어야만 한다.

Data parallelism
4명의 플레이어가 있는 예제는 AVX machines에서 연산 잠재력의 50%를 낭비할 것이다. 명백히, 우리는 좀 더 많은 일이 필요하다. 효율적인 SIMD code는 많은 data parallelism을 요구하는데, 거기에서, operations의 한 sequence가 많은 inputs들에 대해 실행된다. 100% 효율성에 도달하는 것은 그 input array size가 4 또는 8의 배수일 것을 요구한다. 그러나 어떤 큰 input array size에 대해, 우리는 이 최적의 것에 매우 가깝게 되고, AVX 성능은 간단히 SSE 성능의 두배가 된다.

data-parallel 알고리즘에 대해, SIMD register에 있는 스칼라들 각각은 한 'thread'에 대한 데이터를 보유한다. 우리는 register에 있는 slots들을 lanes라고 부른다. 이 입력 데이터는 stream이라고 불려진다.

Into the Mud
만약 너가 C++ 프로그래머라면, 너는 아마도 기본 형들에 대해 친숙하다: char, short, int, float, 등. 이러한 각각은 특정한 크기를 갖는다 : char는 8bits, short는 16, int와 float 32. Bits은 그냥 bits이고, 그러므로 float와 int 사이의 차이는 해석에 있다. 이것은 우리가 어떤
못된 것들을 하게 한다:


int a;
float& b = (float&)a;

이것은 한 정수와, a를 가리키는 한 float reference를 만든다. 변수 a와 b가 이제 같은 메모리 위치를 차지하기 때문에, a를 바꾸는 것은 b를 바꾸고, 역으로도 된다. 이것을 얻는 대안은 union을 사용하는 것이다:


union { int a; float b; };

또 다시, a와 b는 같은 메모리 위치에 있다. 여기에서 또 다른 예시가 있다:


union { unsigned int a4; unsigned char a[4]; };

이 번에, 네 개의 chars의 작은 배열은 32-bit integer 값 a4에 중복된다. 우리는 이제 배열 a[4]를 통해 a4에 있는 개별 bytes를 접근할 수 있다. a4가 이제 기본적으로 4개의 1-byte 'lanes'를 가지고 있다는 것을 주목해라. 이것은 어느정도 우리가 SIMD로 얻는 것과 유사하다. 우리는 심지어 32개의 1-bit value로서 a4를 사용할 수 있고, 그리고 그것은 32개의 boolean values를 저장하는 효율적인 방법이다.

SSE register는 128bit 크기이고, 만약 4개의 float를 저장하도록 사용한다면 __m128로 이름 지어졌고, ints에 대해서는 __m128i로 이름지어져있다. 편의성을 위해, 우리는 __m128를 'quadfloat'로, __m128i를 'quadint'로 발음할 것이다. 그 AVX versions은 __m256 ('octfloat')이고 __m256i ('octint')이다. SIMD types를 사용할 수 있기 위해서, 우리는 몇 가지 헤더들을 인클루드 할 필요가 있다:


#include "nmmintrin.h" // for SSE 4.2
#include "immintrin.h" // for AVX

__m128 변수는 4개의 floats를 포함하고, 그래서 우리는 또 다시 union trick를 사용할 수 있다:


union { __m128 a4; float a[4]; };

이제 우리는 편리하게 __m128 vector에 있는 개별 floats를 접근할 수 있다.

우리는 또한 quadfloat를 직접 생성할 수 있다:


__m128 a4 = _mm_set_ps(4.f, 4.1f, 4.2f, 4.3f);
__m128 b4 = _mm_set_ps(1.f, 1.f, 1.f, 1.f);

그것들을 함께 더하기 위해서, 우리는 _mm_add_ps를 사용한다:


__m128 sum4 = _mm_add_ps(a4, b4);

그 __mm_set_ps와 _mm_add_ps 키워드들은 intrinsic이라고 불려진다. SSE와 AVX intrinsics은 모두 단일 assembler instruction으로 컴파일 된다; 이러한 것들을 사용하는 것은 우리가 필수적으로 우리의 프로그램에서 어셈블러 코드를 직접 작성하는 것을 의미한다. 가상으로 무든 스칼라 연산에 대한 intrinsic이 있다:


_mm_add_ps(a4, b4); 
_mm_sub_ps(a4, b4);
_mm_mul_ps(a4, b4);
_mm_div_ps(a4, b4);
_mm_sqrt_ps(a4);
_mm_rcp_ps(a4); // 역수

AVX에 대해, 우리는 비슷한 intrinsics를 사용한다: 간단히 _mm 대신에 _mm256을 앞에 붙인다. 그래서 _mm256_add_ps(a4, b4) 등이 된다.

SSE와 AVX 명령어들에 대한 완전한 overview는 여기에서 찾을 수 있다:

https://software.intel.com/sites/landingpage/IntrinsicsGuide/

너는 안전하게 2000년도 이후에 생상된 어떤 CPU가 4.2까지의 SSE를 지원한다는 것을 가정할 수 있다. AVX와 특히 AVX2는 좀 더 최근의 기술이다; 지원하는 프로세서들에 대한 목록을 위해 위키피디아를 체크해라:

https://en.wikipedia.org/wiki/Advanced_Vector_Extensions

SIMD in C#
이전 섹션은 C++의 사용을 가정했었다. 운 좋게도, SIMD는 C#에서도 또한 이용가능하다. 비록 그 구현이 훌륭하지 않을지라도.

SIMD 지원은 System.Numerics.Vectors package에서 찾아질 수 있다. 처음에, 너는 Nuget Package Manager를 통해 어셈블리의 최신 버전 (글 쓰는 시점에는 4.3.0)를 추가할 필요가 있다.

여기에 몇 가지 실험을 위해 우리가 사용할 작은 C# program이 있다.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
using System.Numerics;
namespace test { class P { static void Main(string[] args)
{
   var lanes = Vector<int>.Count;
   int[] a = { 1, 2, 3, 4, 5, 6, 7, 8 };
   int[] b = { 1, 1, 1, 1, 1, 1, 1, 1 };
   int[] c = new int[a.Length];
   for( int i = 0; i < a.Length; i += lanes )
   {
      var a8 = new Vector<int>(a, i);
      var b8 = new Vector<int>(b, i);
      (a8 + b8).CopyTo(c, i);
}}}}

Main 함수에서 첫 번째 line은 너의 하드웨어에 의해 지원되는 lanes의 개수를 결정한다. C#은 너가 SSE 또는 AVX를 선택하도록 하지 않는다. 대신에 하드뒈어 독립적으로 설계되었다. Lanes은 4 또는 8일 수 있고, 미래 하드웨어에는 아마도 16일 수 있다. 만약 우리가 line 5에 breakpoint를 넣는다면, lanes은 최근의 CPU에서 4로 나올 것이다. 이것을 해겨랗기 위해, Project -> Properties로 가서, Build tab를 선택하고, `Prefer 32-bit`를 비활성화해라. 이제 lanes은 AVX2-capable processor에서 8이 될 것이다.

8개의 lanes으로 Vector<int>는 octint이다. 그 loop에서 octint a8은 배열 a의 8개의 값으로 채워지고, b8은 배열 b의 8개의 값으로 채워진다. 그것들은 함께 더해지고, 그 결과는 배열 c에 복사되고, 그 배열은 최종 결과를 가지게 된다.

질문 : C# compiler는 실제로 SIMD code를 만들어내는가? 이것을 결정하기 위해서, 우리는 깊게 파고들 필요가 있다.








SSE from Songho Ahn

http://www.songho.ca/misc/sse/sse.html

SIMD (Single Instruction, Multiple Data, "seem-dee"로 발음되는) 연산은 단일의 명령어로 병렬로 여러개의 데이터를 처리한다. 이것은 한 번에 4개의 연산이 중요한 성능 개선을 만든다.

SSE는 single-precision floating-point 연산을 위해 8개의 새로운 128-bit 레지스터들 (xmm0 ~ xmm7)를 정의한다. 각 레지스터는 128비트 길이이기 때문에, 우리는 32-bit floating-point numbers를 총 4개 저장할 수 있다 (1-bit는 부호, 8-bit는 exponent, 23-bit는 mantissa).

Scalar and Packed Instructions
SSE는 연산에 대해 두 가지 유형을 정의한다; scalar와 packed. Scalar operation은 least-significant data element (0 ~ 31비트)에 대해서만 작동하고, packed operation은 병렬로 모든 4개의 원소들을 연산한다. SSE 명령어들은 scalar operations에 대해 -ss (Single Scalar)의 접미사를 갖는다. 그리고 packed operations에 대해 -ps(Parallel Scalar)를를 갖는다.

스칼라 연산에 대해 xmm0에 있는 상위 3개의 원소들은 바뀌지 않는다는 것을 주목해라.

Data Movements
너가 알아야 하는 첫 번째 것들은 메모리에서 xmm registers로 어떻게 데이터를 복사하는 가이고, SIMD 연산 후에 xmm registers로부터 너의 어플리케이션으로 그 결과를 다시 얻는 가이다. 그 데이터 이동 명령어들은 memory와 xmm registers들 사이에서 scalar와 packed data를 움직인다.

movss : single floating-point data를 복사한다.
movlps : 2개의 floating-point data (low packed)를 복사한다.
movhps : 2개의 floating-point data (high packed)를 복사한다.
movaps : 정렬된 4개의 floating-point data를 복사한다 (fast)
movhlps : 2개의 high elements를 low position으로 복사한다.
movlhps : 2개의 low elements를 high position으로 복사한다.

movaps는 메모리에 있는 데이터가 더 좋은 성능을 위해서 정렬된 16byte boundray에 있어야만 하는 것을 요구한다. Data Alignment에서 데이터를 어떻게 정렬하는지에 대해 더 많이 읽어라. movhlpsmovlhps에 대한 source와 destination에 대한 operands는 xmm registers 여야만 한다.

Arithmetic Instructions
산술 명령어들은 산술 연산을 수행하기 위해 2개의 operands (registers 또는 메모리)를 요구하고, 첫 번째 register에 그 결과를 write한다. 그 source operand는 xmm register 또는 memory일 수 있찌만, 그 destination operand는 xmm register여야만 한다.

addss/ps
subss/ps
mulss/ps
divss/ps
rcpss/ps -> y = 1/x
sqrtss/sqrtps
rsqrtss/rsqrtps -> y = 1/sqrt(x)
maxss/maxps
minss/minps

Shuffle Instruction
shufps는 2개의 operands와 1개의 mask를 요구한다. shufps는 그 마스크를 기반으로 각 operand (register)로부터 2개의 원소들을 선택한다. 그 첫 번째 operand에 대한 2개의 원소들은 destination register에 있는 lower 2 elements로 복사되어지고, second operand에 있는 2개의 원소들은 그 destination register에 있는 더 높은 2개의 elements로 복사된다.

shufps 명령어를 사용하여, 너는 어떤 순서든 어떤 4개의 데이터 원소들을 shuffle할 수 있다. shufps의 빈번한 사용성은  broadcast, swap 그리고 rotate이다.

Broadcast
이것은 single data원소의 모든 4개의 fields를 복사한다. 가능한 masks는 다음이다
00h : least significant data element를 Broadcast한다.
55h : 두 번째 데이터 원소를 Broadcast한다.
AAh : 세 번째 data element를 Broadcast한다.
FFh : most significant data element를 Broadcast한다.

shufps xmm0, xmm0, 0h
shufps xmm0, xmm0, 55h

Swap
이 명령어는 1Bh mask로 데이터 원소들의 순서로 반대로 바꾼다
shufps xmm0, xmm0, 1Bh

Rotate
그것은 data elements의 왼쪽 또는 오른쪽 회전을 수행한다. 데이터를 왼쪽 회전으로 shift하기 위해서 93h를 사용해라. 그러면 most significant data element는 least significant position으로 움직여진다. 데이터를 오른쪽으로 shift하기 위해 39h를 사용해라. 그러면 least significant data element는 most significant position으로 움직여진다.

shufps xmm0, xmm0, 93h
shufps xmm0, xmm0, 39h

Unpack
unpcklps는 2개의 operands의 각각으로부터 2개의 lower elements를 복사하고 interleave한다. unpckhps는 2개의 operands의 각각으로부터 2개의 higher elements를 복사하고 destination register에 interleaves한다.

unpcklps xmm0, xmm1
unpckhps xmm0, xmm1

Comparision Instructions
비교 명령어들은 2개의 operands를 비교하고, destination register에 true (모두 1를) 또는 false (모두 0을) 설정한다. source operand는 xmm register 또는 메모리가 될 수 있찌만, 그 destination은 xmm register가 되어야만 한다.

x = y | cmpeqss / cmpeqps
x != y | cmpneqss / cmpneqps
x < y | cmpltss / cmpltps
x !< y | cmpnltss / cmpnltps
x <= y | cmpless / cmpleps
x !<= y | cmpnless / cmpnleps

Bitwise Logical Instructions
논리 명령어들은 bitwise 논리 연산을 packed floating-point elements에 대해 수행한다. 일반적은 사용은 숫자를 음수화하거나 절대값으로 바꾸는 것이다.

AND - andps
OR - orps
XOR - xorps
AND NOT - andnps

Absolute Value
절대값 연산을 수행하기 위해, source register에서 most significant bit (sign bit)에 0을 저장하고, 나머지 비트들에 1를 저장해라. 그러고나서, AND operation을 수행해라 : number & 7FFFFFFFh.

andps xmm0, xmm1

Negate
음수화를 수행하기 위해, most significant bit에 1를 저장하고, 나머지에는 0을 저장해라. 그러고나서 XOR 연산을 수행해라 : number ^ 8000000h.

Conversion
변환 명령어들은 floating-point 숫자를 정수로 또는 역으로 변환한다.

Float to integer with rounding - cvtss2si / cvtps2pi
Float to integer with truncation - cvttss2si / cvttps2pi
Integer to float - cvtsi2ss / cvtpi2ps

그 packed operations들인 cvtps2pi, cvttps2pi 그리고 cvtpi2ps는 병렬로 4개가 아닌 2개의 숫자들을 바꾼다. 왜냐하면 MMX 레지스터들 (mm0 ~ mm7)은 64bit 길이 (2 * 32bit)이기 떄문이다. 그러므로, XMM registers에서 두 개의 상위 elements를 변환시에 사용되지 않는다.

cvtss2si eax, xmm0
cvtsi2ss xmm0, eax
cvtps2pi mm0, xmm0
cvtpi2ps xmm0, mm0

Streaming memory Instructions
SSE는 prefetching의 사용을 통해서 read-miss latency가 실행을 중복하도록 한다. 그리고 그것은 write-miss latency가 streaming stores를 통해 실행을 overlapping함으로써 줄어들도록 한다.

Prefetch Instructions
그 prefetch instructions은 그 프로그램이 그 데이터를 실제로 필요하기전에 L1 and/or L2 cache에 데이터를 가져오는 cache hints를 제공한다. 이것은 data access latency를 최소화한다. 이러한 명령어들은 비동기적으로 실행된다. 그러므로, 프로그램 실행은 prefetching하는 동안 멈추지 않는다.

prefetcht0 : t0 hint를 사용해서 데이터를 메모리에서 L1과 L2 caches로 옮긴다.
prefetcht1 :  t1 hint를 사용해서 데이터를 L2 cache로 옮긴다.
prefetchnta :  non-temporal aligned data를 메모리에서 L1 cache로 직접 옮긴다 (L2를 지나치고).

AMD athlonXP, Intel Pentium4 또는 더 높은 CPUs들은 automatic cache prefetching을 포함하고 있다는 것을 주목해라. 그러므로, 너의 코드에서 이러한 명령어들을 직접 호출하는 것은 반드시해야 하는 것은 아니다.

Streaming Store Instuctions
store move 명령어들은 streaming하는 것은 캐시를 업데이트하지 않고 메모리에 직접적으로 non-temporal data를 저장한다. 이것은 cache pollution을 최소화하고, 캐시와 XMM registers사이의 불필요한 bus bandwidth를 최소화한다. 왜냐하면 그것은 write miss에 대해 write-allocate를 하지 않기 때문이다.

Non-temporal은 data가 긴 간격으로 (한 번 참조되고, 즉각적인 미래에 재사용되지 않는) 불규칙적으로 접근된다는 것을 의미한다. 예를들어 3D graphics에서 vertex data는 매 프레임마다 재 생성된다. Write-allocate는 cache miss가 발생할 때 data가 cache에 쓰여지는 것을 말한다.

movntps : XMM register에서 memory로 직접적으로 4개의 non-temporal floating-point elements를 옮기고, 캐시를 지나친다. 그 메모리 주소는 정렬된 16-byte boundaries여야만 한다.

movntq : XMM register로부터 메모리에 non-temporal quadword (2개의 정수, 4개의 shorts, 8개의 chars)를 옮기고, 캐시를 지나친다.

movntps [edi], xmm0
movntq [edi], mm0

Store Fence
sfencesfence 명령어 이전에 어떤 store 명령어에 대한 데이터가 이후의 store instruction전에 메모리에 쓰여질 거라는 것을 보장한다.

다음의 inline assembly 예제는 source에서 destination array로 4개의 float data (16-byte block)를 복사하는 것을 보여준다.

// move 4 floats (16-bytes) at once
__asm
{
mov ecx, count        // # of float data
chr ecx, 2               // # of 16-byte blocks (4 floats)
mov edi, dst           // dst pointer
mov esi, src           // src pointer

loop1:
movaps xmm0, [esi]       // get from src
movaps [edi], xmm0      // put to dst

add esi, 16
add edi, 16

dec ecx                      // next
jnz loop1
}

Detecting SSE support
cupid 명령어는 그 processor가 SSE를 지원하는지 안하는지를 탐지하는데 사용될 수 있다. 대부분의 x86 processors들은 요즘에 cpuid 명령어를 지원한다. 그리고 이것은 CPU 정보와 지원되는 기능들을 반환한다. 너의 CPU가 cpuid 명령어를 지원하는지 결정하기 위해, EFLAGS에서 bit 21를 수정하려고 해보아라. 만약 bit 21이 바뀔 수 있다면, cpuid는 호출될 수 있다.

cupideax=01h로 호출하는 것은 edx register에 표준 feature flags를 반환한다. SSE는 만약 edx register의 bit 25 (least significant bit에서 26번째 비트)가 1이라면 지원된다. 게다가, bit-26은 SSE2 support를 위한 것이고, bit-23은 MMX support를 위한 것이다.

이것은 SSE support와 다른 features를 탐지하는 매우 간단한 프로그램이다 : cupid_msvc.zip.
(이 프로그램은 그것에 MSVC에 특정한 inline assembly codes를 사용한다는 것에 주목해라)

Example of Inline Assembly
다음의 프로그램은 MSVC inline assembly에서 SSE 사용의 한 예시이다. 그것은 모든 위의 SSE 명령어들에 대한 예제 코드를 포함한다.

소스와 바이너리를 다운로드해라 :  sse_msvc.zip

2019년 10월 15일 화요일

NVIDIA G-Sync Review

https://www.anandtech.com/show/7582/nvidia-gsync-review

그것은 CES에서 거의 12달 전에 시작했었다. NVIDIA는 GeForece Experience를 발표했는데, 그것은 너가 플레이하는 게임들에서 너의 PC를 위한 최적의 graphics settings을 골라주는 문제에 대한 소프트웨어 솔루션이다. 콘솔 게임들로는, 그 개발자는 이미 visual quality와 frame rate의 올바른 균형인 것을 고르도록 했다. PC에서, 이러한 결정권은 최종 사용자에게 넘겨진다. 우리는 몇 게임들이 이용가능한 그래픽 옵션들을 제한하여 그 문제를 시도하려 해결했지만, 그것을 제외하고, 많이 널리퍼진 관심을 보지 않은 것이 문제이다. 결국, PC 게이머들은 설정을 만지작 거리는데 익숙하다 - 그것은 경험의 예측된 부분이다. PC gaming user base를 넓히려는 시도로서 (다음 세대 console wins의 부재에 의해 어느정도 동기부여 받은), NVIDIA는 GeForece Experience를 내놓았다. NVIDIA는 이미 넓은 범위의 NVIDIA 하드웨어에 걸쳐서 수 많은 게임들을 테스트 했다. 그래서 그것은 각 게임/PC 조합에 대해 무엇이 가장 좋은 설정인지에 대한 좋은 아이디어를 가지고 있다.

또한 CES 2013에서, NVIDIA는 Project Shield를 발표했는데, 나중에 그냥 Shield로 다시 이름 붙여졌다. 어느정도 이상하지만놀랍게 멋진 portable Android gaming system은 또 다른 기능의 역할을 했다 : 그것은 너의 TV에서 PC 게임들을 플레이하도록 사용될 수 있는데, 너의 PC로부터 직접 스트리밍 한다.

마침내, NVIDIA는 조용히 (최근에 매우 조용히는 아니고), Valve와 함께, 그것의 SteamOS and Steam Machine efforts에 참여했다 (인정하건데, AMD도 그랬다).

내 견해로는, 그것은 확실히, NVIDIA가 콘솔 게임의 어떤 측면들을 PC로 가져오려는 것처럼 보인다. 너는 더 나아가서, NVIDIA가 더 높은 퀄리티의 그래픽스와 더 높은 프레임율을 집중하기 보다는 여러 방식들로 게이밍을 향상시키기 위해 매우 동기부여 받은 것처럼 보인다고 말할 수 있다.

이러한 모든 것은 결국 이해가 된다. ATI와 AMD가 완전히 통합하면서, 그리고 Intel이 마침내 그래피스를 (어느정도) 진지하게 여기면서, NVIDIA는 앞으로 나아가는 산업에서 관련되도록 (그리고 주가 되도록) 좀 더 많은 것을 할 필요가 있었다. 간단히 좋은 GPUs를 만드는 것은 지금까지 그 회사만 필요할 것이다.

NVIDIA의 최신 시도는 G-Sync인데, 지원되는 NVIDIA graphics card에 의해 주도되는 semi-variable refresh rate를 활성화 하는 hardware solution이다. 그 전제는 이해하기에 간단하다. Displays와 GPUs는 본질적으로 비동기적으로 컨텐츠를 업데이트 한다. 한 display panel은 고정된 간격으로 (그것의 refresh rate로) 그 자체를 업데이트 하는데, 보통 대부분의 패널들에 대해 60 times per second (60 Hz) 이다. 게임에 특화된 displays는 120Hz 또는 144Hz의 더 높은 refresh rates를 지원할지도 모른다. 반면 GPUs는 가능한 빠르게 frames를 렌더링하고, 그것들이 처리될 때 마다 그것들을 display에게 보여준다.

너가 refresh 중간에 도착한 한 frame을 가질 떄, 그 display는 동시에 그 스크린에 여러 프레임들의 일부를 그리게 된다. 동시에 여러 frames의 일부를 그리는 것은 visual artifacts, or teras의 결과를 만들 수 있고, 개별 프레임들을 분리한다. 너는 tearing이 screen에 스크롤되는 것처럼 수평의 lines/artifacts된 것을 눈치챌 것이다. 그것은 매우 산만하다.

GPU와 display가 sync를 맞추어 tearing을 피할 수 있다. vsync를 활성화하는 것은 이것을 한다. 그 GPU는 panel의 refresh rate로 display에 sync를 맞추어 frames들을 전달할것이다. Tearing은 사라지지만, 너는 새로운 artifact를 얻는다 : stuttering.

한 게임의 각 frame의 내용은 급격하게 변할 수 있기 때문에, 그 GPU의 frame rate는 비슷하게 가변적일 수 있다. 또 다시, 우리는 GPU가 그 display로 sync없이 한 프레임을 보여주길 원하는 상황에 있는 우리자신을 발견하게 된다. vsync가 활성화 되었을 때, 그 GPU는 다음 refresh period까지 그 frame을 전달하는 것을 기다리는데, 이것은 그 중간에서 한 반복적인 프레임을 만들어 낸다. 이 반복된 frame은 그 자체로 stuttering으로서 명백해진다. 너가 완벽하게 너의 refresh rate와 일치하지 않는 frame rate를 가지는 한, 너는 보이는 stuttering의 잠재성을 얻게 된다.


G-Sync는 두 세계의 최상의 것을 제안한다고 주장한다. 간단히 말해서, G-Sync는 GPU가 새로운 프레임으로 준비가 될 때 까지, 그 display가 refresh 하는 것을 기다리도록 만들려고 시도한다. tearing도 없고, 어떠한 stuttering도 없이 - 그냥 buttery smoothness가 되도록. 물론, G-Sync display가 있는 NVIDIA GPUs들에서만 이용가능 하다. 항상 그렇듯이, 악은 세부사항에 있다.

How it Works
G-Sync는 hardware solution이고, 이 경우에, 그 hardware는 G-Sync가 활성화된 display안에 있다. NVIDIA는 G-Sync board를 위해 display의 scaler를 바꾸는데, 그 panel과 timing controller (TCON)를 손대지 않는다. display chain에서 그것의 physical location에도 불구하고, 현재의 G-Sync board는 실제로 hardware scaler의 특징을 가지지 않는다. 그것의 의도된 목적을 이ㅜ해서, 어떠한 scaling hardware의 결여는 큰 문제가 아니다. 너가 panel을 움직이고 모든 scaling duties를 처리하는 가능한 한 개 이상의 GPU를 가질 것이기 때문이다.

G-Sync는 그 display의 VBLANK (vertical blanking interval)를 조작하여 작동한다. VBLANK는 현재 프레임의 마지막 line을 rasterizing하는 display와 그 다음 프레임의 첫 번째 line을 그리는 display 사이의 시간 주기이다. 이것은 한 interval이라고 불려지는데, 이 주기동안 어떠한 screen updates가 발생하지 않기 때문에 그 display는 static하게 남아 있는데, 다음의 것을 그리기 전에 현재 프레임을 보여주고 있게 된다. VBLANK는 CRT 시절의 남응ㄴ 부분인데, 거기에서 CRTs에게 그 display의 top에서 scanning을 시작할 시간을 주는 것이 필수적이였다. 그 interval은 오늘날 LCD flat panels에서 남아 있는데, 비록 그것이 기술적으로 필수적일지 아니라도. display안의 G-Sync module은 GPU가 새로운 것을 가져다 줄 준비가 될 때 까지 display가 현재의 frame을 유지하도록 VBLANK를 수정한다.

G-Sync가 활성화된 display로, 그 모니터가 현재 프레임을 그리는 게 끝났을 때, 다음의 draw process가 시작하기전에 그것은 그 GPU가 display를 위해 준비된 또 다른 것을 가질 때 까지 기다린다. 그 delay는 순수하게 VBLANK interval를 조정하여 제어디ㅗㄴ다.

근데 너는 VBLANK 조작으로만 그렇게 할 수 있다. 현재의 구현에서, NVIDIA가 단일 프레임을 유지할 수 있는 가장 긴 시간은 33.3ms (30Hz)이다. 만약 그 다음 프레임이 그 때 까지 준비가 되지 않았다면, 그 G-Sync module은 그 display에게 그 마지막 frame을 다시 그리라고 말할 것이다. 상한은 이 시점에서 panel/TCON에 의해서 제한되는데, 오늘날 이용가능한 유일한 G-Sync monitor는 6.94ms(144Hz)까지 올라간다. NVIDIA는 그 144Hz 제한이 G-Sync limit이 아니라, panel limit이라고 언급했다.

그 G-Sync board는 그 자체로 FPGA와 DDR3 memory 768MB 특징을 가진다. NVIDIA는 on-board DRAM이 너가 일반적으로 디스플레이 안에서 보는 scaler보다 더 크지 않다고 주장한다. 그 추가된 DRAM은 부분적으로 메모리에 대해 좀 더 많은 bandwidth를 고려하는데 필수적이다 (추가적인 physical DRAM devices). NVIDIA는 많은 것들을 위해 그 메모리를 사용하는데, 그것 중의 하나는 이전 프레임을 저장하는 것이다. overdrive calculations에 대해 들어오는 frame에 대해 비교될 수 있게 하기 이ㅜ해서이다.

그 첫 번째 G-Sync module은 DisplayPort 1.2에 대해서 output만을 지원하는데, 비록 기술적으로 NVIDIA가 미래 버전의 HDMI/DVI에 대한 지원을 추가하는 것을 막는 것은 없을지라도. 유사하게, 현재 G-Sync board는 audio를 지원하지 않지만, NVIDIA는 그것이 미래 버전에 추ㅏㄱ될 수 있다고 주장한다 (NVIDIA의 생각은 여기에서, 대부분의 게이머들이 그들의 displays에 통합된 스피커보다는 다른 어떤 것을 원할 것이라는 것이다). 그 첫 번째 G-Sync 구현의 마지막 한계는, 그것은 오직 LVDS의 displays에만 연결될 수 있다느 ㄴ것이다. NVIDIA는 G-Sync module의 다음 버전에서 V-by-One 지원을 활성화할 계획인데, 비록 그것이 eDP support를 또한 활성화하는 것을 막을게 없을지라도.

G-Sync를 활성화 하는 것은 작지만 frame rate에 측정할만한 성능의 영향으 락진다. 그 GPU가 G-Sync가 활성화된채로 한 프레임을 렌더링한 후에, 그것은 그 GPU가 scan out의 중간에 scan을 하지 않을 것으 보장하기 위해 그것이 VBLANK period에 있는지 아닌지를 보기위해 display를 조회하기 시작한다. 그 조회는 약 1ms가 걸리는데, 이것은 v-sync on에 비교하여 3-5% 성능 영향을 가진다. NVIDIA는 그 polling을 전적으로 제거하려고 작업중이지만, 현재 그것이 처리되는 방법이다.


... 하드웨어에 대한 주 내요이라 생략...

2019년 10월 14일 월요일

Fast Sync & SLI Updates : Less Latency, Fewer GPUs

https://www.anandtech.com/show/10325/the-nvidia-geforce-gtx-1080-and-1070-founders-edition-review/13

2012년의 Kepler와 GTX 680 이후로, GPU 개발에서 NVIDIA의 side projects들 중 하나는 input lag를 줄이는 방법들을 고안하는 것이다. NVIDIA의 뛰어나고 (그리고 다재다능한 front man) Tom Petersen 엔지니어의 조심스러운 시야아래에서, 그 회사는 그 문제를 다루기 위해 몇년 간 두 개의 다른 기술들을 도입했다. Kepler는 adaptive v-sync를 도입했다 - 그 frame rate가 refresh rate아래로 내려갈 때, v-sync를 동적으로 비활성화하는 능력이다.  그리고 2013년에 물론, 그 회사는 그들의 G-sync variable refresh rate technology를 도입했다.

그 때 이후로, Tom의 team은 v-sync의 규칙을 바꿀 또 다른 방법을 아직 작업 중이다. Pascal과 관련된 것은 NVDIA가 Fast sync라고 부르는 새로운 v-sync 모드이다. 그리고 v-sync를 유지하면서 input lag를 줄이는 또 다른 방법을 제안하도록 설계되었다.

Fast Sync가 완전 새로운 아이디어가 아니라, 오히려 오래된 아이디어의 현대의 그리고 좀 더 일관적인 태도라는 것을 주목하는 것이 흥미롭다 : triple buffering. 현대에서, triple buffering은 순차적인 frame queue로서 작동하는 3개의 내부 버퍼인데, 몇 게임들과 비디오 카드들은 triple buffering은 조금 다르게 처리했다. 3개의 버퍼들을 순차적인 queue로서 사용하기 보다는, 그것들은 대신에 항상 가장 오래된 buffer에 덮어쓰곤 했다. 이 작은 변화는 input lag에 잠재적으로 중요한 변화를 가졌었다. 그리고 만약 너가 old school triple buffering과 친숙하다며 너는 어디에서 이것이 발생하는지를 안다.

Fast Sync로, NVIDIA는 driver level에서 old school triple buffering을 구현했고, 또 다시, 현대의 그리픽 카드로 사용가능하게 만들었다. Fast Sync를 구현하는 목적은 refresh rate보다 더 높은 frame rate를 만들 수 있는 현대의 게임에서 input lag를 줄이기 위해서 인데, NVIDIA는 구체적으로 CS:GO과 다른 그래픽적으로 간단한 twitch games들을 타겟으로 한다.

그러나 어떻게 Fast Sync가 실제로 input lag를 줄이는가? 이것을 좀 더 들어가기 위해, 내가 아래에서 작성한 2009년의 old school triple buffering에 좋은 글이 있다. 7년 후에, 그 이름을 제외하고 기술적 세부사항은 여전히 NVIDIA의 Fast Sync 구현과 정확하다.

What are Double Buffering, V-sync, and Triple Buffering
한 컴퓨터가 한 모니터에 어떤 것을 display하길 원할 때, 그것은 그 스크린이 어떻게 보내져야 하는지의 그림을 그리고, (우리가 한 버퍼라고 부를) 이 사진을 모니터에게 보낸다. 옛날시절에, 오직 한 개의 버퍼가 있었고, 그것은 지속적으로 모니터에게 그려지고 보내지는 둘 다가 되었다. 이 접근법에 몇 가지 이점들이 있지만, 매우 큰 단점들이 있다. 가장 뚜렷하게, display에 있는 objects들이 업데이트 될 때, 그것들은 종종 깜빡 거린다.

그 같은 buffer에 drawing하는 동안 reading하는 것의 문제와 싸우기 위해, 최소한 double buffering이 이용된다. double buffering 뒤에 있는 아이디어는 그 컴퓨터가 오직 한 버퍼에만 ("back" buffer라고 불려지는) 그리고 그 다른 버퍼를 ("front" buffer라고 불려지는) 것을 스크린에 보낸다. 그 컴퓨터가 그 back buffer를 다 그리고나서, 그 drawing을 하는 그 프로그램은 buffer "swap"이라고 불려지는 것을 한다. 이 swap은 어떠한 것도 움직이게 하지 않는다 : 그 두 버퍼들의 이름만 바꾼다: 그 front buffer가 back buffer가 되고, back buffer가 front buffer가 된다.

buffer swap 이후에, 그 software는 새로운 back buffer에 그리기 시작할 수 있고, 그리고 그 컴퓨터는 그 다음 buffer swap이 발생할 때 까지 그 모니터에 새로운 front buffer를 보낼 수 있다.

double buffering의 이 형태에서, 한 swap은 언제든지 발생할 수 있다. 그것은 그 컴퓨터가 모니터에 데이터를 보내는 동안, 그 swap이 발생할 수 있다는 것을 의미한다. 이것이 발생할 때, 그 스크린의 나머지는 그 새로운 front buffer가 포함하는 것을 따라 그려진다. 만약 그 새로운 front buffer가 이전의 front buffer와 충분히 다르다면, "tearing" 이라고 알려진 visual artifact가 보일 수 있다.  이러한 유형의 문제는 갑자기 빠르게 움직일 때의 high framerate FPS 게임들에서 종종 보일 수 있다. 빠른 움직임 때문에, 모든 frame은 매우 다르고, drawing하는 동안 한 swap이 발생할 때, 그 불일치는 크고, 산만해질 수 있다.

tearing과 싸우는 가장 흔한 접근법은 그 monitor가 또 다른 image가 준비될 때 까지 swap buffers를 기다리는 것이다. 그 모니터는 완전히 그것에게 보내진 것을 그리고, 다음 vertical refresh cycle이 시작하려 한 후에 준비가 된다. Vertical refresh로 buffer swaps를 Synchronizing하는 것은 V-sync라고 불려진다.

V-sync를 활성화 하는 것은 tearing을 고치지만, 그것은 또한 게임의 내부 frame rate를 최대 monitor의 refresh rate로 설정한다 (일반적으로 대부분의 LCD panels에 대해 60Hz). 이것은 비록 그 게임이 60 FPS로 작동하지 않을지라도, 성능에 해를 줄 수 있다. 동기화 효과를 위해 추가된 인공적인 지연이 여전히 있을 것이기 때문이다. 성능은 모든 프레임이 16.67ms (1초의 1/60)보다 더 길게 걸리는 half cases 경우에 거의 줄어들 수 있다. 그러한 경우에, frame rate는 그 게임이 60 FPS로 작동해야 한다는 사실에도 불구하고, 30FPS로 떨어질 것이다. 그러나, tearing의 제거와, framerate의 일관성은 V-sync 없는 double buffering이 보여줄 수 없는 추가된 부드러움에 기여한다.

Input lag(입력 지연)은 V-sync가 활성화 되었을 때 좀 더 문제가 된다. 이것은 왜냐하면 도입된 artificial delay(인공 지연)이 실제로 발생했을 때 (그 프레임이 그려지고 있을 때)와 그것이 스크린에 보일 때 사이의 차이를 증가시키기 때문이다. Input lag는 항상 존재하지만 (스크린에 현재 발생하는 것을 즉각적으로 그리는 것은 불가능하다), 그 trick은 그것을 최소화하는 것이다.

double buffering으로 우리가 가진 옵션들은 V-sync없이 tearing같은 가능한 visual problems와 부정적으로 성능에 영향을 미치고, V-sync가 활성화되어 input lag를 증가시킬 수 있는 artificial delay 사이의 선택이다. 그러나 걱정할 것 없이, quality or actual performance에서 희생없이 두 개의 세계의 최상의 것들을 합치는 옵션이 있다. 그 옵션은 triple buffering이다.

그 이름은 많은 것을 준다 : triple buffering은 두개 대신에 세 개의 버펃르을 사용한다. 이 추가적인 버퍼는 그 컴퓨터에게 한 버퍼가 모니터에 보내지는동안 그 버퍼가 locked 되도록 유지할 충분한 space를 제공한다 (tearing을 피하기 위해서). 또한 그 소프트웨어가 그것이 가급적인 가능한 빠르게 그리는 것을 방해하지 않도록 하면서 (심지어, 한 개의 locked buffer와 함께, 그 소프트웨어가 앞 뒤로 bounce할 수 있는 두 개가 여전히 있다). 그 소프트웨어는 그 두 개의 back buffers 사이에 앞뒤로 그리고, refresh마다, 그 front buffer는 가장 최근에 완료되어 완전히 렌더링된 프레임을 포함한 back buffer에 대해 swapped 된다. 이 것은 그래픽 카드의 메모리에서 추가 공간을 잡아먹지만, 적어도 현재 그래픽카드가 512MB를 가지면서, 이 추가 공간은 더 이상 실제 문제가 아니다.

다시 말해서, triple buffering으로, 우리는 정확히 같은 높은 실제 성능을 얻고, V-sync가 비활성화된 설정의 비슷한 감소된 input lag를 얻는다. 그리고 동시에 visual quality를 얻고, V-sync가 활성화되었을 때의 부드러움을 얻는다.

그러나, 그 소프트웨어가 triple buffering할 때, 두 개의 back buffers에 scenes 뒤의 전체 시간 동안 여전히 그리고 있다는 것을 주목해라. 이것은 그 front buffer swap이 발생할 때, double buffering과 V-sync와 ㄷ르게, 우리는 artificial delay가 없다는 것을 의미한다. V-sync가 없는 double buffering과 다르게, 우리가 한 완전히 렌더링된 프레임을 모니터에게 보낸다면, 우리는 중간에 다른 프레임으로 바꾸지 않는다.

Fast Sync의 최종 결과는 v-sync와 input lag에 관련하여, 올바른 케이스들에서 우리가 효과를 볼 수 있다는 것이다. v-sync가 꺼진 것 처럼 끊임없이 프레임들을 렌더링하고, 그러고나서 가장 최근의 frame을 잡고, 나머지를 버려서, Fast Sync는 v-sync가 발생시키는 전통적으로 높은 input lag 없이 tearing을 방지하기 위해  v-sync가 여전히 사용될 수 있다는 것을 의미한다.

실제의 input lag benefits은 몇 가지 요인들에 의존할 것인데, frame rates와 display refresh rates를 포함한다. 그 최소한의 input lag는 한 프레임을 그리는데 걸리는 시간의 양이다 - v-sync가 꺼진 것처럼 - 그러고나서 너는 실제로 그 프레임을 display하기 위해서 다음 refresh interval를 기다려야만 한다. 평균의 법칙 덕분에, 그 frame rate가 높을 수록 (렌더링 시간이 짧을 수록), 한 프레임이 screen refresh 직전에 준비되는 odds(?)가 더 좋아지는데, 이것은 input lag의 평균량을 줄여준다. 이것은 frame draws사이의 완전한 16.7ms와 전통적인 double buffered setup이 있는 경우에 60Hz displays에 특히 해당한다.

NVIDIA의 예제 수치들은 높은 speed camera를 가진 CS:GO를 모니터링하여 얻어졌다. 나는 이러한 수치들을 확인할 수 없다 - 최대한의 마케팅 노력으로, 그것은 best-case scenario일 수 있다 - 그러나, 그들의 chart가 비합리적인 것은 아닌데, 특히 만약 그들의 v-sync example이 3-deep buffer를 사용한다면. Input lag는 v-sync 없이 더 높을 것이지만, v-sync를 켜는 것 보다 더 낮다 (잠재적으로 훨씬 그렇다). 결국, 최대한의 장점은 매우 높은 프레임율인데, 이것은 NVIDIA가 CS:GO 같은 게임들을 타게팅한 이유이다. refresh rate보다 더 높은 frame rate * times은 최상긔 결과를 만들기 때문이다.

위에서 말해진 것과 함께, 나는 Fast Sync가 순수하게 input lag에 관한 것이고, 부드러움(smoothness)을 다루지 않는다는 것에 주목할 것이다. 사실, 그것은 덜 smooth하게 만들지도 모르는데, 왜냐하면 그것은 본질적으로 frames을 떨어뜨리기 때문이다. frames 사이의 simulation time의 양은 변할 수 있다. 그러나, 높은 프레임율로, 이것은 문제가 되지 않을 것이다. 한 편, Fast Sync는 v-sync의 전력을 아끼는 이점 모두를 잃는 것을 의미한다; 프레임 사이에 쉬고 시간이 지나는 기회를 가지는 GPU보다는, 그것은 이제 full speed로 전체 시간동안 v-sync가 꺼진 것처럼 렌더링하고 있다.

마지막으로, Fast Sync가 NVIDIA의 다른 input lag reduction 기술과 어떻게 맞는지 명확히하는 것은 아마 유용하다. Fast Sync는 Adaptive V-Sync 또는 G-Sync를 대체하지 않지만, 오히려 그것들을 보상한다.


  • Adaptive V-Sync : framerates가 refresh rate보다 낮을 때, v-sync를 선별적으로 비활성화 하여, input lag를 낮춘다.
  • G-Sync : 한 프레임이 준비 되었을 때, 그 display의 maximum refresh rate까지 screen을 refresh하여, input lag를 줄인다.
  • Fast Sync : framerate가 그 display의 refresh rate를 넘을 때 GPU를 지연시키지 않고 input lag를 줄인다.
Fast Sync는 구체적으로 frame rates가 display의 refresh rate를 초과할 때의 경우를 다룬다. 만약 그 frame rate가 refresh rate보다 낮다면, 그러면 Fast Sync는 아무것도 하지 않는다. 왜냐하면 그것은 시작할 single frame을 렌더링하는데 한 refresh interval보다 더 걸리기 때문이다. 그리고 이것은 대신에 Adaptive Sync가 들어오는 곳이다. 만약 바란다면.

한 편, G-Sync와 함께 할 때, Fast Sync는 그 frame rate가 그 display의 maximum refresh rate를 초과할 때에만 중요해진다. 대부분의 G-Sync 모니터에 대해, 이것은 120-144Hz이다. 이전에 max refresh rate보다 높은 G-sync 옵션들은 tear하거나 (no v-sync) 또는 GPU를 대기시키는데 (v-sync), 그래서 이것은 또한 G-Sync에 대해 tear-free input lag option을 제공한다.























2019년 3월 12일 화요일

지금까지 만든 것 (2019년 1월 기준)

1월에 프로젝트를 끝내고 이것저것 하다보니 업데이트를 해놓지 않았다.
이런 기록을 남겨두는 것은 이전으로부터 어떻게 발전해왔는지를 보여주기 때문에
종종 남겨놓는 것이 좋다고 생각한다.

1. 현재 만들어놓은 3D 게임 엔진의 최종 상황이다.
Sponza 모델과 Nanosuit를 통해 렌더링 했다.
영상은 최종 Scene을 만들기 위해 Light의 여러 Property를 수정하는 것을 보여준다.
아직 Culling 기능을 개발하지 않고 최적화가 이루어지지 않아 한 프레임 연산 속도는 느리다. Object Picking With Broad Phase(Dynamic AABB Tree), Model Loading (using Assimp) and Rendering + Model Diffuse/Specular/Emissive/Normal/Height Map (Normal Mapping, Parallax Occlusion Mapping), Light Property 편집, Light Shadow 설정 등이 들어갔다. learnopengl.com에서 배웠던 것을 Deferred Rendering 기반으로 바꾸면서 나의 이해를 증진시켰다. Post-Processing으로 Gamma Correction, Bloom, SSAO가 들어갔다.

2. 물리엔진 기반을 Box2D로 바꾸기로 결심하면서 Box2D-lite 코드를 이해하고 DirectX11 로 포팅해서 시뮬레이션 세팅하여 찍은 영상이다. 현재 Box2D 관련 자료들을 공부하며 이 기반을 다시 철저히 이해하기 위해 공부중이다. 2D를 이해하고 3D로 넘어갈 예정이다.

3. Terrain Rendering + Physics
Terrain을 Triangle Strip으로 한 번의 draw call로 렌더링했다.
그리고 Mesh vs Sphere의 Collision Detection and Response를 특히, Collision Detection 부분을 Bullet Physics를 참고해서 따라했다. 그 Bullet Physics의 코드를 이해하고 내 엔진에 적용할 수 있어서 매우 도전적이고 재미있는 경험이였다.
여기에서 BroadPhase(Dynamic AABB Tree) -> Mesh Quantization -> Early Exit (AABB vs AABB(of sphere) -> Narrow Phase(Triangle vs Sphere, Closest point on triangle using barycentric coordinate from Real Time Collision Detection book). 등의 많은 것들이 적용되었다. Mesh vs Box를 구현하려다 포기하고 Box2D로 돌아가게 되는 계기였는데, 어쨋든 가치있는 작업이였다.

2018년 12월 31일 월요일

게임 일정 (2주차)

내가 원하는 대로 진도가 나가지 않아서, 일정을 항상 봐야겠다.

분기점1 : Terrain and Model
분기점2 : Graphics Details
분기점3 : Physics and Animation
분기점4 : Game UI and Effect
분기점5 : Optimization and Convenience

이번주부터가 분기점 2를 시작해야 하지만, 분기점 1의 Terrain도 끝내지 못했다. 어차피 완벽한 것은 추구하지 않는다. 그러니 나는 내가 가야할 길을 천천히 가보도록 하자.

Terrain Rendering 쪽에 좀 걸리는 것이 있는데 아래의 튜토리얼로 끝내자.


Terrain Physics가 이제 가장 큰 발목인데, 아래의 것을 보고 시도해보고.


만약 내가 원하는게 없다면 BulletPhysics 코드를 처음부터 까기 시작해서 해야할 것이다.
그래서 이런식으로 해서, Terrain Rendering과 Physics를 끝내자.

Terrain Editing에 대해서 내가 만들고 싶은 기능은
- 클릭으로 해당 Terrain 지역 높이 위 아래 높게 만들기
- Terrain Texture 바꾸기
- 그림 그리듯이, 마우스로 그림 Terrain 위에 Painting 하는 기능 (근데 별 필요없을듯)

여튼 내 생각엔 분기점 1~2는 반드시 하고 싶은 것이니, 분기점 3 ~ 5가 기한 내에 못하더라도 천천히 구현해 나가도록 하자.

어차피, 나는 계속 이런 내용들을 공부해 나갈 내용이니, 취업하더라도 계속 이어나가면 되니까.

2018년 12월 23일 일요일

12월 말의 게임 일정 다시

1월 말까지가 나의 프로젝트 기간이다.
그 때 까지 목표 달성을 위해서 다시 현재 상황을 체크하고,
목표를 재정립할 필요가 있다.

원래는 그래픽스쪽 learnopengl 에 있는 튜토리얼들을 Deferred Shading 기법으로 모두 다 구현할려고 했지만, 현재 시간상 다 구현하고 있으면 진척을 보이기가 어렵다 그래서 순서를 섞으려고 한다. 이제 그 튜토리얼에서 남은 것은

- Normal/Parallax Mapping
- Bloom
- SSAO
- PBR
- Object Outline with Stencil
- HUD (letters)
- Debug System
- Particle Effect

이 정도가 되는데 내 생각엔 이거하고 있다가 1월 중순이 되어버릴 것 같다.

그래서 1월 말까지 내가 하려는 것의 좀 더 진척이 보이게끔 하려면 저것만 먼저 하고 있는게 아니라 다른 거를 해야겠다.

그래서 순서를 정하도록 하겠다.

우선 Terrain쪽을 먼저할 것이다. 그러면 좀 더 진척이 확연하게 보이고 잘 보일 것 같다.

Terrain에서 내가 하려고 했던 것은

- Terrain Rendering
- Terrain Physics
- Terrain Editing
- Ray Marching Procedural Terrain Generation and Physics

이것이 되는데, 4번은 가장 어려우니까 나중에 시간나면 하는 것으로 하자.

그리고 물리엔진 쪽인데,

현재 할 수 있는지는 모르겠찌만, David Baraff 논문의 마지막 파트 공부하면서 Simultaneous 그 쪽을 공부하고. Box2D와 Bullet Physics 가 가지고 있는 안정성을 조금이라도 확보하고 싶다.

그리고 Inverse Kinematics쪽을 공부해서 Animation을 직접 다하고 싶은데, 현실 여건상 어려우니 assimp 라이브러리를 통해 fbx? 라는 걸 이용하여 animation을 구현하도록 해야겠다.

그리고, 현재 내가 만드는 것은 게임을 만드는 하나의 Tool이라고 볼 수 있는데 그  Tool을 통해서 환경을 만들면 또 다시 최적화되게 게임을 실행시킬 수 있는 실행 엔진을 만들어서 하는 게 좋을 것 같다. 그리고, 스크립트? 같은 것으로 쉽게 게임을 만들도록 하면 더 좋을 것 같다.

지금까지 말한것도 아주 많은 것이지만 이제 순서를 정하고 거기에 따라 가보자. 다 완성하지 못할지라도, 하는 부분들이 불안전할지라도, 공부하기에 가치있고 결국에는 다 내 것들로 만들어야 할 것이다.

그래서 현재 이제 할 것들은

=======================================
- Terrain Rendering
- Terrain Physics
- Terrain Editing

- Model Loading and Editing about texture, rotation and so on.
- Physics with models loaded by the above feature

분기점1. 여기까지 했다면, 내 최종 데모를 위한 Scene을 잘 꾸밀 수 있을 것 같다. 그래서 Terrain, Model 배치가 가능할 것 이다.
=======================================

- Normal/Parallax Mapping
- Bloom
- SSAO
- PBR

분기점2. 이걸 통해서 그래픽 디테일을 올려 좀 더 멋진 디테일을 가지게 한다.
================================

- David Baraff Article
- Box2D, BulletPhysics Library
- Animation using Assimp and FBX(?)
- Inverse Kinematics (가능하다면)

분기점3. 이걸 통해서 물리를 통해 게임이 더 자연스럽고 애니메이션을 통해
하고 싶은게 거의 되어있을 것이다.
===============================

- Object Outline with Stencil
- HUD (letters)
- Particle Effect
- Debug System


분기점4. 게임 시스템에 도움되는 UI와 그래픽 요소들을 추가한다.
이것은 LearnOpenGL 튜토리얼을 따라하기 때문에 마지막에 Debug System도
추가하도록 한다.
==============================

- Porting System From Edit Engine to Execution Engine
- Game Script System (가능하다면)

분기점5. 첫 번째 기능에서, 포팅시스템을 만들어서 이제 edit 기능이 안들어간
게임을 위해 최적화된 실행 엔진을 만들면 데모를 하기에 좋을 것이다.
근데, Game Script System에 있다면 좀 더 편리하게 게임 요소들을
다룰 수 있을텐데, 이건 시간이 가능하다면 하자.
==============================

따라서 정리하자면,

분기점1 : Terrain and Model
분기점2 : Graphics Details
분기점3 : Physics and Animation
분기점4 : Game UI and Effect
분기점5 : Optimization and Convenience

이렇게 된다. 1월 말까지 이제 한달 남은거나 똑같으니, 각 분기점을 1주일안에 돌파해야 한다. 솔직히 불가능하리라 생각이 드는데, 어쨋든 하는데 까지하고. 1월 말 지나면, 하는데 까지 포트폴리오 정리를하고 취업 준비하면서 조금씩 해나가면 될 듯 하다.




2018년 11월 6일 화요일

Game Development Progress

여기에 게임 개발한 것들이 점점 발전되어가는 것을 저장하고 기록하려고 한다.

181107 Just RigidBody Simulation
Box vs Box, Box vs Sphere, Box vs Plane.


181110 deferred rendering first try
100 lights, 200fps, tone mapping HDR, gamma correction.




181114 Dynamic AABB Tree(Bounding Volume Hierarchy)
First Implementation of Broad Phase. There are lots of rooms to improve.
But I think that this first try is good. 


181122 Make Ray From Screen Click + Object Picking Through RayCast



181126 월요일

181127 Simple Translation Gizmo Implementation

181224
I added lots of feature for a while.
You can check the progresses of my engine in twitter
https://twitter.com/Chan_Lee3211

The latest video is..






2018년 11월 1일 목요일

Game 공부할 주제들 정리

Computer Graphics Principles and Practice (3rd Edition)
5 An Introduction to Human Visual Perception
8 A Simple Way to Describe Shape in 2D and 3D
9 Functions onf Meshes
15 Ray Casting and Rasterization
17 Image Representation and Manipulation
20 Textures and Texture Mapping
21 Interaction Techniques
22 Splines and Subdivision Curves
23 Splines and Subdivision Surfaces
24 Implicit Representation of Shape
25 Meshes
26 Light
27 Materials and Scattering
28 Color
29 Light Transport
32 Rendering in Practice
33 Shaders
34 Expressive Rendering
35 Motion
36 Visibility Determination
37 Spatial Data Structures
38 Modern Graphics Hardware

OpenGL SuperBible (7th Edition)
1 Introduction
3 Following the Pipeline
4 Math for 3D Graphics - Interpolation, Lines, Curves and Splines
5 Data - Buffers, Uniforms, Shader Storage Blocks, Atomic Counters, Textures
7 Vertex Processing and Drawing Commands
8 Primitive Processing : Tessellation(example terrain rendering), Geometry Shader
9 Fragment Processing and the Framebuffer
10 Compute Shaders
11 Advanced Data Management
12 Controling and Monitoring the Pipeline
13 Rendering Techniques
14 High-Performance OpenGL
15 Debugging and Stability

Real Time Rendering (3rd Edition)
2 The Graphics Rendering Pipeline
3 The Graphics Processing Unit
5 Visual Appearance
6 Texturing
7 Advanced Shading
8 Area and Environmental Lighting
9 Global Illumination
10 Image-Based Effects
11 Non-Photorealistic Rendering
12 Polygonal Techniques
13 Curves and Curved Surfaces
14 Acceleration Algorithms
15 Pipeline Optimization
16 Intersection Test Methods
17 Collision Detectino
18 Graphics Hardware

Game programming Gems2
4.2 Simplified Terrain Using Interlocking Tiles
4.5 Direct Access Quadtree Lookup
4.8 Applying Decals to Arbitrary Surfaces
5.4 Generating Procedural Clouds Using 3D Hardware
5.7 Impostors: Adding Clutter

Game programming Gems3
1.13 Real-Time Input and UI in 3D Games
2.5 Constrained Inverse Kinematics
2.7 Coping with Friction in Dynamics Simulations
3.6 Tactical Path-Finding with A*
3.7 A Fast Approach to Navigation Meshes
3.8 Choosing a Relationship Between Path-Finding and Collision
4.2 Fast Heightfield Normal Calculation
4.4 Fast and Simple Occlusion Culling
4.5 Triangle Strip Creation, Optimization, and Rendering
4.8 Improved Deformation of Bones
4.16 Procedural Texturing

Game programming Gems4
1.1 The Science of Debugging Games
1.4 Designing and Maintaining Large Cross-Platform Libraries
1.5 Fight Memory Fragmentation with Templated Freelists
1.6 A Generic Tree Container in C++
1.7 The Beauty of Weak References and Null Objects
1.8 A System for Managing Game Entities
1.9 Address-Space Managed Dynamic Arrays for Windows and the Xbox
2.2 Extracting Frustum and Camera Information
2.3 Solving Accuracy Problems in Large World Coordinates
2.4 Nonuniform Splines
2.5 Using the Covariance Matrix for Better-Fitting Bounding Objects
2.6 The Jacobian Transpose Method for Inverse Kinematics
3.1 Ten Fingers of Death : Algorithms for Combat Killing
3.2 Vehicle Physics Simulation for CPU-Limited Systems
3.3 Writing a Verlet-Based Physics Engine
3.4 Constraints In Rigid Body Dynamics
3.5 Fast Contact Reduction for Dynamics Simulation
3.6 Interactive Water Surfaces
3.7 Fast Deformations with Multilayered Physics
3.8 Model Analysis for Fast, Stable Deformation
4.1 Third-Person Camera navigation
5.11 Heat and Haze Post-Processing Effects
5.12 Hardware Skinning with Quaternions
5.14 Fast Collision Detection for 3D Bones-Based Articulated Characters
5.15 Terrain Occlusion Culling with Horizons

Game Programming Gems6
1.4 Geographic Grid Registration of Game Objects
1.5 BSP Techniques
1.9 Faster File Loading with Access-Based File Reordering
2.1 Floating-Point Tricks
2.2 GPU Computation in Projective space using Homogeneous Coordinates
2.3 Solving Systems of Linear Equations Using the Cross Product
2.5 Exact Buoyancy for Polyhedra
2.6 Real-Time Particle-Based Fluid Simulation with Rigid Body Interaction
5.1 Synthesis of Realistic Idel Motion for Interactive Characters
5.2 Spatial Partitioning Using an Adaptive Binary Tree
5.3 Enhanced Object Culling with (Almost) Oriented Bounding Boxes
5.4 GPU Terrain Rendering
5.6 Interactive Fluid Dynamics and Rendering on the GPU
5.7 Fast Per-Pixel Lighting with Many Lights
5.9 Practical Sky Rendering for Games
5.10 High Dynamic Range Rendering Using OpenGL Frame Buffer Objects

https://blog.mapbox.com/drawing-antialiased-lines-with-opengl-8766f34192dc

2018년 10월 26일 금요일

중간 점검..

이번 10월달까지 하는 것은 GPED 책을 다시 공부하여 게임 물리를 제대로 하는 것이다.

이제 10월 말에 다 다가왔는데 아직 좀 남았다.
하지만, 난 제대로 해서 실력을 늘리는 것이기에 조급해하지 말자.
하지만, 제한을 두어 좀 더 집중하게 만들고 싶기도 하다.

어쩃든, 지금 앞으로 해야할 과제가 주어졌으니 그걸 달성하기 위해서 할 것들을 정리한다.
과제는 Rigid Body Simulation + Imgui 라이브러리 사용해서 박스들을 떨구어서 물리 시뮬레이션이 잘 작동되는 것을 보여주는 것이다.

아직 충돌탐지 부분을 이제 공부하고 있다. 그래서 앞으로 할 일들은

1. RTCD Chapter 4(50페이지 분량), Chapter 5 (100페이지 분량)
2. GPED Chapter 13 (충돌탐지)
3. David Baraff 논문 충돌탐지 섹션

4. Chris Hecker 3
5. GPED Chapter 14 (접촉 해결)
6. GPED Chapter 15
7. David Baraff 논문 마무리

8. Rigid Body Simulation + Imgui

이렇게 된다.

사실 1번이 지금 가장 어렵고 분량이 많다. 2번 같은 경우엔 1번을 기반으로 하기 때문에, 1번만 제대로 한다면 2번은 빠르게 끝내고, 3번도 금방 끝낸다.
1번을 최소 4일 그 이상이 될 수도 있다.

그리고 4~7을 이틀정도 두고 싶다.

그러면 물리엔진이 어느정도 문제 없이 돌아갈 것이고, 8번을 아주 짧은 시간내에 만들 수있을 것이다.

여튼 첫 단추인 1번을 잘 공부하여 지식을 함양하는 것이고,

4번을 잘 공부해서 Contact Resolving을 잘 이해해서 567의 3D에서 해결을 잘 이해하여 넘어가는 것이다.

거기에다가 현재 코드에서 잘못된 부분들을 고치도록 해야한다.

2018년 10월 7일 일요일

게임 개발 일정

현재 Physics Engine을 이제 막 눈에 보이게끔 작동만 시켰다.

사실, 아직 이해되지 않은 것들이 많이 있는데, 전체를 다 끝내고,
이해 안되는 것들을 하나씩 보면서 공부해나가면 이해할 수 있다고 생각이든다.

어쨋든 하나의 milestone에 도달했으니, 현재 상황을 파악하고, 어떻게 나아갈지
정하는게 좋을 것 같다.

내가 봤을 때 현재 하나씩 해야할 것들을 알아보자.


  1. GPED 책 따라하며, RigidBody Demo들 따라서 구현하기. 코드 오류 찾아서 수정하기
  2. GPED 처음부터, 물리 이해안된 부분 공부하기 + 필요한 이론과 관련 지식들 자료 찾아서  공부하기
  3. 내가 이제까지 배운 그래픽스를 GUI통해서(시간을 줄이기 위해, imgui라는 유명한 라이브러리 활용) 조절할 수 있게 프로그램 만들기 (+ 그래픽스 내용 다시 공부하고 이해하기)
  4. 내가 이제까지 배운 물리를 GUI통해서 조절할 수 있게 프로그램 만들기 (RigidBody들의 parameter Setup, Object Pick and Arrangement, Physics Engine Debug Mode) + (앞의 그래픽스엔진과 합하여서)
  5. Terrain Generation + Rendering + Physics 공부하기
  6. 4번까지 만든 프로그램에 5번의 내용을 조작할 수 있게 추가하기
  7. Map Tool 제작하기
  8. Inverse Kinematics 공부 및 적용
  9. 게임 개발.
하나하나가 빡세고 시간이 필요한 것들이다. 그래서 시간제한을 두어서 이것을 달성해나가야 겠다고 생각이든다. 그래야 내가 원하는 목표가 될 수 있다.

1번과 2번은 이번 10월 말까지 하도록 해야겠다. 1번과 2번을 제대로 하게되면은, 나중에 필요한 여러가지 지식들을 한 번에 또 공부할 수가 있어서 많은 시간이 요구되는 것들이다.

3번 ~ 5번까지 11월 말까지 마무리 하도록 한다. 4번까지 해도, 어느정도 유용한 프로그램이 만들어져가고 있을테니, 개발 동기부여와 재미면에서 많은 것들을 줄 수 있을 것이다.

6번 ~ 8번까지 12월 말까지 마무리 하도록 한다. Map Tool를 만들었다면, 게임 개발 시에 내가 원하는 Scene들을 빠르게 만들어서, 테스팅할 수 있을 것이다. 그리고 게임 Reality 에 중요한 것중 하나인 Inverse Kinematics를 공부하고 구현한다.

그리고 1월 말까지, 이 때까지 공부한 것과 만든 프로그램으로 게임 데모를 만든다.

2018년 9월 24일 월요일

FPS Quaternion Camera Implementation (쿼터니언 카메라 구현)

Reference Link:
http://graphics.stanford.edu/courses/cs348a-17-winter/Papers/quaternion.pdf
https://www.gamasutra.com/view/feature/131686/rotating_objects_using_quaternions.php
https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation

Result GIF:


Camera Header:
/*
 This code is originally based on a tutorial code of learnopengl.com.
 I converted this euler camera to quaternion camera
*/

#ifndef __CHAN_CAMERA_H__
#define __CHAN_CAMERA_H__

#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>

enum class Camera_Movement
{
 FORWARD,
 BACKWARD,
 LEFT,
 RIGHT
};

// Default camera values
const float SPEED = 10.0f;
const float SENSITIVITY = 0.01f;
const float ZOOM = 45.0f;

class chanCamera
{
public:
 glm::vec3 Position;
 glm::quat Orientation;
 float RightAngle;
 float UpAngle;

 // Camera options
 float MovementSpeed;
 float MouseSensitivity;
 float Zoom;

 chanCamera
 (
  glm::vec3 position = glm::vec3(0.f, 0.f, 0.f),
  glm::vec3 up = glm::vec3(0.f, 1.0f, 0.f)
 );
 chanCamera(float posX, float posY, float posZ);

 glm::mat4 GetViewMatrix();
 void ProcessKeyboard(Camera_Movement direction, float deltaTime);
 void ProcessMouseMovement(float xoffset, float yoffset, bool constrainPitch = true);
 void ProcessMouseScroll(float yoffset);

private:
 void updateCameraVectors();
};

#endif


Camera Source:
#include "chanCamera.h"
#include <iostream>

chanCamera::chanCamera(glm::vec3 position)
 :
 MovementSpeed(SPEED),
 MouseSensitivity(SENSITIVITY),
 Zoom(ZOOM)
{
 Position = position;
 Orientation = glm::quat(0, 0, 0, -1);
 RightAngle = 0.f;
 UpAngle = 0.f;
 updateCameraVectors();
}

chanCamera::chanCamera(float posX, float posY, float posZ)
 : MovementSpeed(SPEED), MouseSensitivity(SENSITIVITY), Zoom(ZOOM)
{
 Position = glm::vec3(posX, posY, posZ);
 Orientation = glm::quat(0, 0, 0, -1);
 RightAngle = 0.f;
 UpAngle = 0.f;
 updateCameraVectors();
}

glm::mat4 chanCamera::GetViewMatrix()
{
 // You should know the camera move reversely relative to the user input.
 // That's the point of Graphics Camera
glm::quat reverseOrient = glm::conjugate(Orientation);
 glm::mat4 rot = glm::mat4_cast(reverseOrient);
 glm::mat4 translation = glm::translate(glm::mat4(1.0), -Position);

 return rot * translation;
}

void chanCamera::ProcessKeyboard(Camera_Movement direction, float deltaTime)
{
 float velocity = MovementSpeed * deltaTime;

 glm::quat qF = Orientation * glm::quat(0, 0, 0, -1) * glm::conjugate(Orientation);
 glm::vec3 Front = { qF.x, qF.y, qF.z };
 glm::vec3 Right = glm::normalize(glm::cross(Front, glm::vec3(0, 1, 0)));

 if (direction == Camera_Movement::FORWARD)
  Position += Front * velocity;

 if (direction == Camera_Movement::BACKWARD)
  Position -= Front * velocity;

 if (direction == Camera_Movement::LEFT)
  Position -= Right * velocity;

 if (direction == Camera_Movement::RIGHT)
  Position += Right * velocity;
}

void chanCamera::ProcessMouseMovement(float xoffset, float yoffset, bool constrainPitch)
{
 xoffset *= MouseSensitivity;
 yoffset *= MouseSensitivity;

 RightAngle += xoffset;
 UpAngle += yoffset;

 updateCameraVectors();
}

void chanCamera::ProcessMouseScroll(float yoffset)
{
 if (Zoom >= 1.f && Zoom <= 45.f)
  Zoom -= yoffset;

 if (Zoom <= 1.f)
  Zoom = 1.f;

 if (Zoom >= 45.f)
  Zoom = 45.f;
}

void chanCamera::updateCameraVectors()
{
 // Yaw
 glm::quat aroundY = glm::angleAxis(glm::radians(-RightAngle), glm::vec3(0, 1, 0));

 // Pitch
 glm::quat aroundX = glm::angleAxis(glm::radians(UpAngle), glm::vec3(1, 0, 0));

 Orientation = aroundY * aroundX;
}

This Quaternion Camera is based on the Euler to Quaternion Conversion.

The code process will be these :

  1.  User Mouse Input -> ProcessMouseMovement() -> updateCameraVectors() ->   GetViewMatrix()
  2.  User Keyboard Input -> ProcessKeyboard() -> GetViewMatrix()


I will explain the details of each method. However, User Mouse Input and User keyboard Input methods are skipped. If you need it, it had better refer other materials about taking user input. Or, I studied it on learnopengl.com in the Getting Started - Camera Chapter. You can get informations on taking user input with the detailed source code.
My code is based on the code of learnopengl.com.

1. ProcessMouseMovement()
After taking your mouse input, you can know the offsets of mouse position, especially, x and y delta. the delta value x means that A user wants to look at  the left or right side. the delta value y means that A user wants to look at up or downside. In the Graphics Camera Movement, the delta value x can be an angle around y-axis, what is called Yaw. In addition, the delta value y can be an angle around x-axis, what is called Pitch.

Since i'm trying to implement Graphics Camera For FPS-like Game, I don't consider the an angle around z-axis, what is called Roll. However, If you need it, you can insert the roll into your code, after you recognize the structure and essence of this quaternion camera.

Therefore, In one sense, ProcessMouseMovement() let you know how camera should rotate by Yaw and Pitch. That's it. once again, I mean that Yaw and Pitch are angles around y-axis and x-axis.

2. updateCameraVectors()
This is kind of the center of quaternion camera. while it looks simple, you need to know lots of quaternion algebra to understand what role each line is taking. Actually, for me, It took me lots of times to understand quaternion camera and write those lines.

http://graphics.stanford.edu/courses/cs348a-17-winter/Papers/quaternion.pdf

I recommend you to read the article above. The article is so kind that I can approach the level I want to about the quaternion. According to the article, we can define quaternion like this :



the angleAxis function of glm make that kind of equation of quaternion. That's the reason why there are glm::radians function and glm::vec3(). glm::vec3 means the u in the equation, which is the rotation axis.

Since we need to implement the rotation of Yaw and Pitch, I wrote the two lines of quaternion angle axis.

And the last line is the resulting rotation quaternion and the orientation of camera in quaternion format. Actually, you can accumulate the rotation in quaternion to multiply the quaternion.

https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation#Using_quaternion_as_rotations

you can know the mathematics of rotation accumulation in quaternion with this link.

But There is a point you have to care for, which is the order of multiplication. Because quaternion is not commutative in multiplication, you have to consider the order. However, There are particular orders of camera when you are using Euler to Quaternion Conversion.

https://www.gamasutra.com/view/feature/131686/rotating_objects_using_quaternions.php

In this article, it requires us to use the order quaternion = Yaw Pitch Roll. It meas Y, X, Z rotation in the order. 

Once again, because quaternion algebra is not commutative, we have to consider the order of rotations. each of all combinations can generate other rotations.

3. GetViewMatrix()
Now that we get the orientation of camera, it's time to get the view matrix. we can get the rotation matrix directly from the quaternion orientation.

https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation#Quaternion-derived_rotation_matrix

With this article, you can know the mathematics of converting a quaternion to the rotation matrix.

However, Before converting it, We have to conjugate the quaterion orientation. Because, as you know, the graphics camera works reversely with our intention. If we want to look at right side, all the objects in world should move left side. That's the point of why we should conjugate the quaternion orientation.

and then we get the actual rotation Matrix. And you will also need to represent the translation of camera. So, I got the translation matrix. Actually, you can decrease the computation of GetViewMatrix() like this:


glm::mat4 chanCamera::GetViewMatrix()
{
 // You should know the camera move reversely relative to the user input.
 // That's the point of Graphics Camera!
 glm::quat reverseOrient = glm::conjugate(Orientation);

 glm::mat4 rot = glm::mat4_cast(reverseOrient);
 rot[3][0] = -(rot[0][0] * Position.x + rot[1][0] * Position.y + rot[2][0] * Position.z);
 rot[3][1] = -(rot[0][1] * Position.x + rot[1][1] * Position.y + rot[2][1] * Position.z);
 rot[3][2] = -(rot[0][2] * Position.x + rot[1][2] * Position.y + rot[2][2] * Position.z);
 rot[3][3] = 1;

 // glm::mat4 translation = glm::translate(glm::mat4(1.0), -Position);
 // return rot * translation;

 return rot;
}

You can know the mathematics why the code above should be like this in my article https://chanhaeng.blogspot.com/2018/06/the-construction-of-camera-matrix.html.

4. ProcessKeyboard()
In order to translate the camera in World, We need to know two direction, which are Front and Right directions. To get the front direction, we also use the orientation quaternion. The initial direction of Front direction is glm::vec3(0, 0, -1). However, as we move and rotate the camera, the Front direction is not pointing at glm::vec3(0, 0, -1). So, we rotate the glm::vec3(0, 0, -1) with the quaternion. We know how to rotate the vector in the articles I linked above.



so that's the reason why there is a code like this:


glm::quat qF = Orientation * glm::quat(0, 0, 0, -1) * glm::conjugate(Orientation);

so we got the rotated Front vector from the orientation quaternion. and then we got the real front vector with this line.


glm::vec3 Front = { qF.x, qF.y, qF.z };

because the x, y, z is the direction in 3D world, which are the real part of three complex numbers, i, j, k.

and then we can get the Right direction more easily with the rotated Front direction vector. the cross product of Front(actually -Z-axis) and World-Up(actually Y-axis) will give you the Right direction vector. ( cross(Y, Z) = X, cross(Z, Y) = -X, cross(-Z, Y) = -cross(Z, Y) = X).

With these direction vector, you can translate the camera position now.

5. More Study....
Actually, I didn't study all of details on the links i referred. It means that there are lots of contents I have to understand. The thing is slerp, which is interpolating the accuracy of the rotation. I'm not quite used to that kind of notion, but I will study it later. Because the journey from nothing to here is quite hard and took much time for me, I need to record my understandings here. After analyzing more details of quaternion, I will update this article.