Backface Culling

 

백페이스 컬링이란 간단하게 카메라가 볼 수 없는 면을 그리지 않는 것이다.

 

위의 그림의 경우 안쪽 면까지 다 보이는 상태인데 여기서 백페이스 컬링을 적용하게 되면 다음과 같이 보인다.

 

 

게임을 하면서 관심 있는 부분은 우리 눈에 보이는 부분이다.

 

보이는 부분만 그리는 것과 보이지 않는 부분까지 그리는 것의 성능 차이가 상당하고,

게임 속 캐릭터나 배경 오브젝트 같은 경우는 겉 모습만 보이기 때문에 안쪽 부분까지 그릴 필요가 없다.

 

그러므로 3D 엔진에서는 다각형을 그리기 전에

다각형을 구성하는 각 면이 보이지 않는 면인가를 빠르게 판단해야 한다.

 

각 면이 보이지 않는 면인가를 판단하려면, 

삼각형을 보았을 때

0 => 1과 0 => 2로 가는 벡터를 만들어주고,

외적을 시키게 되면은 해당 형태에서 노말 값이 나오게 되는데,

 

Backface Culling Cross Product(백페이스 컬링의 외적 공식)은 다음과 같다.

 

 

여기서 카메라의 정면(-Z 방향)과 노말 값을 내적 해줬을 때 같은 방향이 나오게 되면 면을 그리지 않도록 해주면 된다.

 

Result

 

Perspective Projection

 

먼저 FOV가 60도로 정해져있다고 가정해보면 시야 각은 절반인 30도가 되고, 

절반이라는 부분이 있을 때 여기서 투영할 전체 3D 공간에서 거리를 정하는 것이다.

 

여기서 초점거리는 어떻게 정해지는 것인가?

 

예를 들어서 3D 공간에 여러 가지 물체가 있다고 했을 때 임의의 평면을 하나 만들어서 부착을 시켜주게 되면

Perspective 모드에 맞춰서 3차원에 있는 물체를 2차원에 매핑을 시키는 작업이 필요하게 되는 상황이 발생하게 되는데,

 

이 상태에서 내가 투영할 스크린을 1로 정규화 시키는 것이다.

1로 정규화를 시키게 되면은 초점거리에 대해서는 다음과 같은 공식을 통해서 구할 수가 있다.

이 상태를 가정하고 다음과 같이 평면 밖에 있는 어떤 점을 투영을 시키게 되면 Pndc의 좌표가 나오게 되는데

여기서 Pndc의 좌표는 절반의 크기가 1이기 때문에 1이 넘어갈 수가 없기 때문에

0과 1 사이에 들어온다는 결론이 나게 되는데, 결과적으로는 다음과 같이 비례 삼각형을 하나 더 그리게 되면

뷰 좌표계로 본다고 했을 때 카메라 앞 방향의 길이는 -Zview가 되고,

 

d : -Zview의 비율은

가 나오게 된다.

 

이렇게 해서 Yndc의 값을 구할 수 있게 됐고,

Xndc도 같은 방식으로 구할 수 있는데 여기서 화면 비율이 정사각형이면 아무런 문제가 없는데

 

항상 종횡비(4:3, 16:9)를 가지고 있기 때문에 정사각형과 똑같이 1이라고 가정하고

원을 그렸을 때 정사각형 화면은 제대로 나오지만 종횡 비율 화면은 다음과 같이 찌그러진 형태가 나오게 된다.

 

이렇게 되면 종횡 비율 화면에서 지저분하게 보이기 때문에 이것을 보정하기 위해서는

종횡비를 미리 계산해놓고 이것을 나눠주게 되면은 1의 사이즈가 줄어들게 되는데,

 

이렇게 세로를 기준으로 하고 가로로 똑같은 크기로 나올 수 있게 보정해 주는 작업이 필요해서

a는

가 되고, 최종적으로 다음과 같이 보정이 된 상태로 나오게 된다.

 

이렇게 투영된 X, Y좌표를 구하게 되었고,

다음은 Yndc와 Xndc에 대한 행렬식이다.

y 값을 구할 때 x는 영향을 미치지 않기 때문에 나머지를 0으로 놓고 x에 대해서만 계산을 하면 된다.

그러면 다음과 같이 행렬식의 두 개에 대한 부분에 대해서 채울 수가 있다.

그러면 여기서 x와 y의 공통점은 무엇일까?

분모가 둘 다 -Zview인 것이다.

-Zview가 z랑 연관이 있을 수도 있기 때문에 분모를 뺀 상태로 다음과 같이 넣어본다.

이렇게 되면 뭔지는 모르겠지만 w'가 -Zview가 나오도록 해보는 것이다.

 

그러면 위에 두 식은 다르지만 w' 값을 하나 구할 수 있게 되었고,

x, y 값은 나중에 1/-Zview로 나눠주면 되기 때문에 행렬을 이런 식으로 해놓은 것이다.

 

그 상태에서 이제 마지막으로 z'의 값을 구해야 되는데,

평면에는 x, y 값만 있으면 되기 때문에 z'값은 필요가 없어서 다음과 같은 식으로 해놓아도 상관이 없다.

그러면 무엇 때문에 z'값을 구해야 될까?

 

z 값을 구해야 되는 이유는 카메라 안에서 평면에는 똑같이 그려지지만

z 값이 어떤 깊이에 있는지를 알아야 되기 때문이다.

왜냐하면 점이 같은 위치에 있더라도 서로 깊이가 다르다는 것을 z 값으로 구별할 수가 있기 때문이다.

결론적으로 깊이에 대한 값을 판단해서 앞에 물체가 있으면 바로 뒤에 있는 물체는 그리지 않는 것이다.

 

그런데 만약 깊이 정보가 없으면 어떤 문제가 발생할까?

 

이런 식으로 그리는 경우에는 그리는 순서에 영향을 받는다.

 

매쉬 순서에 의해서 앞에 있는 물체가 먼저 그려지게 되면은

뒤에 있는 물체를 나중에 그렸을 때 무조건 덮어 씌우게 된다.

 

어떤 물체를 그릴 때 삼각형을 먼저 그리게 된다.

여기서 뒤에 있는 삼각형이 앞에 있는 삼각형에 가려져야 되는데,

뒤에 있는 삼각형이 카메라 위치 또는 매쉬 순서에 의해서 가장 나중에 그려지게 되면은 덮어 씌우게 댄다.

 

그래서 깊이 정보가 없이 만들게 되면은 다음과 같이 이도 저도 아닌 물체가 나오게 된다.

이제 z 값에 대해서 어떻게 정할지 생각을 해봐야 한다.

 

위아래, 양옆에 대한 정보는 -1 ~ 1로 규정을 해서 구해놓았고,

z 값의 정보, near랑 far에 대한 정보를 어떻게 줄 것인가에 대한 문제가 있는 것인데.

 

near에 대한 부분도 x, y와 똑같이 -1과 1을 넣어주고 계산하는 것이다.

그러면 z값이 near에 위치한 경우 -1, far에 위치한 경우 1이 나오게 되고,

이것을 기반으로 해서 다음과 같이 방정식을 푸는 것이다.

뷰 좌표계에서 -n 값, 즉 카메라가 있으면 (0, 0, -1, 1)은 카메라가 n 만큼 간 위치에 해당하는 것이다.

그러면 여기서 z 축으로만 간 것이기 때문에 x, y랑은 상관이 없어서 x, y의 값은 0이 되는 것이다.

그런데 여기에 모르는 미지수 두 개가 존재한다.

여기서 모르는 미지수 두 개를 다음과 같이 곱해서 -n이 나와야 하는데 

왜 -n이 나와야 되는 것인가?

 

앞에서 w'를 -Zview로 저장한 이유는 -Zview가 x, y와 연관성이 있기 때문에 w'에 저장을 시키고

나중에 계산할 때 나누는 것이 이 과정에 함축돼있는 것이기 때문이고,

 

네 번째 행에 대해서 이렇게 계산을 할 수 있게 된 것이다.

그래서 아래와 같이 나온 이 좌표들을 -Zview로 나눠줘야 한다.

그렇게 최종적으로 Zview는

-Zview는 

가 나오게 된다.

 

이번에는 far 값을 계산해보자

여기서 far 값은 -f, 1이 되고 w 값은 f로 나와 있는데

z 값은 f이기 때문에 계산 결과가 1이 나오게 되었고,

 

n과 f 서로 k, l이 뭐가 나와야 하는지 모르겠지만

각각 n과 f가 나와야 되는 이유가 밝혀졌다.

그러면 이것을 이제 방정식으로 만들어서 똑같이 l과 f로 나눠서 각각 구해주게 되면은 다음과 같은 공식이 나오게 된다.

이렇게 k와 l에 대한 값을 구했으므로 다음과 같이 최종 투영 행렬을 완성할 수 있게 된다.

그런데 다음과 같은 식을 곱했을 때 나오는 결과는 w를 아직 나누지 않은 상태로 값이 나오게 된다.

이것은 정규화를 시키기 위한 준비 과정이라고 볼 수 있는데,

w 값을 나누지 않은 이 값을 Clip Coordinate라고 한다.

여기서 w의 값은

가 되고 이것은 뷰 좌표계에서 카메라부터의 깊이 값이라고 할 수 있다.

 

그리고 w 값을 나누게 되면 NDC 값이 만들어지게 되고,

 

Normalized Device Coordinate(NDC)

-1 ~ 1값으로 정규화가 된다.

 

지금까지 한 것을 정리해보면 다음과 같다.

 

 

  • 우리가 새롭게 구축할 3D 공간에 대한 설명
  • Y-up Right Handed Coordinate(오른손 좌표계)

    양의 x와 y 축은 오른쪽과 위를 가리키고, 음의 z 축은 앞으로 가리킨다.
    양의 회전은 회전 축에 대해 시계 반대 방향이다.

 

오른손 좌표계가 시계 반대 방향인 이유는 주먹이 쥐어진 상태로 오른손을 들고 엄지손가락만 펴보면 알 수 있다.

 

 

  • X axis(Right), Z axis(Forward) 각각의 용도

X axis의 용도 : 왼쪽, 오른쪽 방향을 구분

Z axis의 용도 : 앞, 뒤 방향을 구분

 

  • 3D Rotation에 대한 고찰
  • 회전이란 공간 변환의 관점에서 어떤 행동인지 기술

    회전이란 물체가 돌아가는 것이 아닌 공간이 돌아가는 것이다.
    수학적으로 설명하자면 벡터 공간 입장에서 보았을 때 공간이 돌아간다고 할 수 있다.

예를 들어

한 이미지를 만들어서 월드 공간에 배치를 하게 되면 이것은 물체를 배치하는 것이 아닌

아핀 공간이라고 하는 점들의 집합 즉, 마지막 차원이 1인 공간 자체가 평행 이동을 해서 월드 공간에 덧 씌워진 것이다.

 

그렇게 덧 씌워진 상태에서 선형 변환을 한다는 것은 그 이미지에 속하는 로컬 공간이 있고,

그 공간 자체에는 두 개의 기둥이 있는데 이것은 x축과 y축이 있는 것이다.

 

그리고 그 공간의 x축과 y축을 변형 시킨다는 것은 그 공간 자체의 기반을 변형시킨다는 것이다.

 

따라서 그 공간은 표준 기저 두개의 조합이라고 설명할 수 있다.

 

그렇다면 이 공간의 회전은 어떤 의미를 하는 것일까?

모습이 변하지 않는 것이라고 할 수 있다.

 

그렇다면 회전을 하기 위해 이 모습이 변하지 않기 위한 조건은 무엇일까?

 

첫 번째 조건은 변환된 표준 기저(x, y)의 크기가 1이면 된다.

하지만 크기가 1이라고 다 변하지 않는 것이라고 할 수 없다.

 

왜냐하면 크기가 1인 상태에서 서로 직교를 해줘야 하기 때문이다.

만약 서로 직교를 해주지 않게 되면

위의 그림과 같은 형태의 모양이 나왔을 때 일그러지게 된다.

 

따라서 회전을 하기 위한 조건에는 크기가 1인 상태에서 서로 직교를 해야 한다는 특징이 있고,

이것을 Rigid Transform이라고 한다.

 

3차원도 마찬가지로 x, y, z 축이 서로 직교한 상태에서 그대로 돌아간다고 보면 된다.

3차원의 회전은 x, y 축의 방향이 어떻게 재설정 됐는지 볼 수 있는데,

이것을 "Orientation(방향의 재설정)"이라고 할 수 있다.

 

x, y, z의 방향이 유지된 상태에서 틀어졌기때문에 회전을 정확하게 표현하는 방법은 딱 한가지가 있다.

그것은 x, y, z 벡터를 꽂아주는 것이다.

x, y, z 벡터를 꽂아주게 되면 가장 정확하게 회전의 상태를 알아낼 수 있게 되는데, 

문제는 이것은 데이터가 너무 많고 직관적이지가 않다는 것이다.

 

이 문제로 인해서 데이터가 적고 직관적인 방식을 보완한 것이 있는데

대표적으로 오일러 각 방식( Yaw(Y), Roll(Z), Pitch(X) )이 있다.

 

이 방식은 언리얼 엔진에서도 사용을 하고 있는데,

언리얼 엔진 같은 경우는 회전에 벡터를 사용하지 않고 오일러 각 방식을 따로 만들어서 데이터를 관리하고 있다.

 

오일러 각 방식을 만들어서 사용하는 이유는?

3D Rotation 값을 매트릭스로 저장하면 모든 변환된 인스턴스에 더 많은 메모리를 사용할 수 있기 때문이다.

 

  • 각 축에 대한 회전 행렬을 생성할 때 왜 Y 축은 반대인지 이유

각 축에 대한 회전 행렬은 다음과 같다.

여기서 x, z 축은 일반적인 방법의 회전 행렬을 사용하고 있지만 y 축은 다르게 되어 있다.

 

먼저 x, z 축을 90도씩 돌려보자

 

x축

 

z축

 

일반적인 방식으로 y 축을 계산해 보면 다음과 같이 나오게된다.

여기서 결과가 [-1, 0, 0]이 나오는데 -1이 나왔다는 것은 반대 방향으로 돌았기 때문이다.

 

이렇게 되면 x, y, z 축을 회전 시켰을 때 사이클이 돌지 않게 되고 회전이 이상하게 꼬일 수가 있다.

그런데 다음과 같이 한쪽이 틀어진 상태로 계산을 하게 되면

 

결과가 [1, 0, 0]이 나오게 되고, 이 상태로 x, y, z 축을 회전 시키게 되면 다음과 같이 사이클이 돌게 된다.

따라서 y의 회전 행렬은 부호를 반대 방향으로 뒤집어서 전치가 되어야 원하는 사이클을 돌릴 수가 있다.

 

  • 3D 회전 행렬

이번에는 R(yaw) R(pitch) R(roll)의 순서로 회전 행렬을 곱해서 최종 로컬 Forward, Right, Up의 값을 구해보자

 

 

  • 카메라 뷰 좌표계

뷰 좌표계는 카메라 뒤쪽으로 향하는 방향이 +z인 좌표계이다. 

 

  • 목표 지점 정보가 주어진다면 이를 토대로 카메라의 회전 행렬을 제작하는 전체 과정을 유도

먼저 카메라가 목표 지점의 방향으로 회전을 시키려면 회전한 값에 대한 3x3 행렬이 필요하고,
카메라의 위치랑 바라봐야 하는 게임 오브젝트의 위치가 필요하다.

 

먼저 벡터의 크기를 1로 만들어 놓고 위치를 지정 해놓고 Z 축으로 정해 놓는다.

그 이후에 벡터를 기준으로 두 개의 직교하는 벡터를 똑같은 순서대로 만들어서 작업을 하면 끝이 난다.

 

그런데 이 벡터만 가지고 직교를 할 때 하나의 직교를 하는 축의 정보를 구할 수 있을까?

문제는 이것이 두 가지 쌍이 존재할 수도 있는데, 밑으로 가는 쌍이 있고, 위로 가는 쌍이 있을 수도 있다.

그래서 벡터만 있으면 카메라가 반대로 뒤집어져 있는 상태인지 위를 보고 있는지 아래를 보고 있는지

알 수 있는 방법이 없다.

그럼 해결 방법은 무엇인가?

 

카메라가 위를 바라볼 건지 아래를 바라볼 건지 정해주기만 하면 된다.

위를 바라본다는 것은 월드의 Y 축을 가져와서 외적을 시키면 된다.

외적을 시키게 되면 두 벡터의 직교한 벡터가 나오게 되는데

그렇게 나온 벡터와 Z 축 벡터를 직교해버리면 나머지 한 축을 구할 수가 있고,

 

그것을 통해서 3x3 행렬을 구할 수가 있다.

 

3축(Forward, Right, Up)을 구해보자

먼저 카메라의 뒤 쪽 방향(+Z)을 Forward라고 할 수 있고

월드의 Y 축에서 오른쪽으로 돌리게 되면 Right가 나오게 된다.

그리고 Forward 방향에서 Right 방향으로 돌리게 되면 Up이 나오게 된다.

그렇게 최종적으로 3축(Forward, Right, Up) 구한 결과는 다음과 같다.

이때 예외적으로 주의해야 할 사항은 카메라의 뒤 쪽 방향을 구했지만 월드의 Y 축과 겹칠 수가 있는데,
이런 경우에는 카메라가 월드의 위를 보거나 아래를 보는 경우가 발생하게 되면

외적 결과가 나오지 않게 되므로 월드의 X축이 되도록 만들어줘야 한다.

 

3축(Forward, Right, Up)을 모두 구했으니 이것을 통해 이동과 회전이 고려된 카메라 행렬을 유도해보자

 

TRS의 역행렬에서 카메라는 크기가 필요 없기 때문에 TR의 역행렬이 되는데

합성 함수의 역행렬은 반대로 되기 때문에 +를 해줘야 하므로

라고 할 수 있다.

 

그리고 회전 행렬의 성질 중에는 Orthogonal matrix라는 것이 있는데

이것이 항등 행렬로 나오기 위해서는 전치를 해주면 된다.

그 얘기는 즉, 전치행렬은 역행렬이라고 할 수 있다. 

 

따라서 회전 행렬은 

가 되는데 이 회전 행렬에서 전치를 해주면 다음과 같이 된다.

 

  • 카메라를 (0, 0, -500)에 두고 원점에 위치한 큐브를 와이어 프레임으로 렌더링 한 이미지

  • 회전 행렬을 적용해 입력에 따라 Yaw, Roll, Pitch 회전한 결과

Yaw

Roll

Pitch

  • 카메라를 (500, 500, -500)으로 이동하고 원점을 향해 보는 구도를 생성

지수 함수를 알기 전에 다음과 같은 지식을 알고 있는 상태로 진행을 하면 좋다.

먼저 알아야 될 것은 자연 상수(Euler's Number)이다.

 

예)

자연 상수e는 원주율처럼 무리수에 해당이 되고, n이 무한대로 커지면 커질수록 e의 값에 가까워진다고 생각하면 된다.

 

쉽게 말해 무리수이자 상수이며, 자연 상수라고 표현할 수 있으며,

 

자연 상수 e는 자연의 연속 성장을 표현하기 위해 고안된 상수라고 할 수 있다.

 

조금 더 구체적으로는 100%의 성장률을 가지고 1회 연속 성장할 때 얻게되는 성장량을 의미한다.

 

또 자연 상수 e라는 것은 e의 범위, e의 값, 무리수 e 이 세 가지를 통해 얻어진다.

자연 상수 e의 특징으로 들어보자면 다음과 같은 식을 미분을 하게 되면 결과는 어떻게 나오게 될까??

 

 

결과는 똑같은 e^x가 나오게 된다.

 

왜냐하면 이처럼 e^x를 미분을 하게 되면 e의 정의의 특성상 자기 자신이 되기 때문이다.

반대로 말하면 미분한 것이 자기 자신이 되는 함수가 e^x라고도 할 수 있다.

 

다른 예를 들어보자

f(x) = cosx을 미분을 하게 되면 f(x) = -sinx가 되고 f(x) = sinx을 미분을 하면 f(x) = cosx가 된다.

 

미분에 대해서 알아보던 중 미분의 경우 앞에 co가 붙은 함수 즉,

cos, csc, cot 이 세 가지 함수를 미분을 하면 결과에 마이너스가 붙는다고 한다.

 

따라서 f(x) = -sinx를 미분을 하게 되면 f(x) = -cosx가 되고, f(x) = cosx를 미분을 하면 f(x) = -sinx가 된다.

 

그럼 이번에는 지수함수(e^x), sinx, cosx 함수를 맥클로린 급수로 전개해보자

  • 지수함수(e^x)

 

  • sinx

 

  • cosx

여기서 왜 cosx를 미분하면 -sinx가 나오고 sinx를 미분하면 cosx가 나오는지 각각 미분을 통해 알아보자

 

  • cos(x)

 

  • sin(x)

 

다음은 sinx + cosx의 맥클로린 급수 전개 결과와 지수함수 전개 결과가 같기 위한 조건은 무엇인지 생각하고,

이것이 오일러 공식임을 정리해보자

  • sinx + cosx

둘이 거의 비슷하긴 한데 한 가지 다른 점은 sinx + cosx는 - - + +가 주기적으로 반복이 된다는 것이다.

 

또 sinx + cosx 와 지수함수(e^x)랑은 묘한 유사 관계가 있다고 볼 수 있다.

 

여기서 오일러라는 수학자가 sinx + cosx 와 지수함수(e^x)랑 같게 만드는 방법을 찾았는데,

그것은 바로 다음과 같이 허수를 사용하는 것이다.

이런식으로 허수를 대입하게 되면 서로 같아진다는 일치 관계를 발견하여 이것을 오일러 공식이라고 정했다고 한다.

 

이제 오일러의 공식으로 유도된 값은 회전에 해당하는 복소수와 동일함을 이용하여,

e^iθ는 또다른 회전의 표현임을 정리해보자.

 

회전

위에 회전 공식을 다음과 같이 바꿔 쓸 수가 있고,

 이것은 e^iθ와 똑같다고 할 수가 있다.

 

이로써 자연 상수의 허수는 회전을 의미한다고 할 수 있다.

 

위의 정리를 활용해 두 회전의 곱은 두 각의 합과 동일한 결과임을 정리해보면

 

가 나오게 되고 이 결과를 보면 처음에 짚고 넘어가야 할 부분의 값과 같은 것을 확인할 수가 있다.

앞에서 설명한 Norm이 1인 복소수

가 복소수 (cosΘ, sinΘ)와 같다고 하였을 때,

 

복소수 z = (cosΘ, sinΘ)의 역수는 z = (cosΘ, -sinΘ)가 나오게 된다.

 

여기서 z = (cosΘ, sinΘ)는 회전 행렬을 의미하고 z = (cosΘ, -sinΘ)는 회전의 역행렬을 의미한다.

 

(cosΘ sinΘ)를 정의해서 (x, y) (cosΘ, sinΘ)를 풀어보면

 

(xcosΘ - ysinΘ) + (xsinΘ + ycosΘ)i

= (xcosΘ - ysinΘ, xsinΘ + ycosΘ) 가 나오게 된다.

 

복소수 회전이 행렬과 동일함을 이용해, 복소수를 행렬로 표현을 하면 다음과 같다.

다음은 Norm이 1인 복소수 (cosα, sinα)와 (cosβ, sinβ)의 곱이 (cos(α+β), sin(α+β))와 동일함을 증명해보자

 

  • (cosα, sinα)와 (cosβ, sinβ) 의 곱

(cosα, sinα) = α만큼 회전

(cosβ, sinβ) = β만큼 회전

 

  a       b        c      d

(cosα, sinα) * (cosβ, sinβ)

= ( ac - bd, ad + bc)

= (cosα cosβ) - (sinα sinβ), (cosα sinβ) + (sinα cosβ)

 

 

  •  (cos(α+β), sin(α+β))

       

  • 허수부를 나타내는 행렬 및 그 특징과 의미
  • cosΘ + sinΘ i(회전 행렬)을 행렬로 표현

이 행렬을 실수부와 허수부로 나누게 되면 아래와 같이 나오게 되고  

나눠진 상태에서 cosΘ와 sinΘ를 앞으로 빼면 다음과 같이 나오게 된다.

a + bi => a * I(실수부) + B * J(허수부)

 

허수끼리 곱해주게 되면

가 나오게 되는데 이것은 허수의 정의 

와 같다고 볼 수 있다.

+ Recent posts