🧐 해당 파트는 게임 개발 환경을 구성하는 컴퓨터 그래픽스(Computer Graphics)를 이해하기 위한 기초 수학의 간단한 개념에 대해 설명하고 있습니다!
혹여나 이해가 잘 안되거나 잘못된 정보를 발견하시게 되었다면 관련해서 피드백 해주시면 정말 감사하겠습니다!
먼저 소실점을 생각하기 전 유클리드 좌표에 대해 설명을 해보도록 하겠습니다.
유클리드 좌표는 3개의 축이 서로 직교하여 각 차원을 표현할 수 있습니다.
라는 설명은 기하학적인 설명이라고 볼 수 있습니다.
수학적인 설명으로 유클리드 좌표를 설명해보라고 하자면 각 축들이 서로 영향을 주지 않는다.
즉, 각 축들에 대한 내적값이 0이 되는 것을 의미한다고 볼 수 있습니다.
기하학적 설명과 수학적 설명의 차이가 뭔가요?
뭔가 기하학적 설명과 수학적 설명의 느낌이 오묘하게 다르죠?
기하학적인 설명은 뭔가 이론을 중심으로 수학적인 요소가 빠지더라도 이해하기 편한 설명이 되는 느낌이지만
수학적인 설명은 수학적인 요소를 통해 이러한 성질은 만족하는걸 설명하는, 즉 공리만 알고있다면 나머지는 논리적으로 문제가 없는 상황이 보장되는 완벽 설명이라고 알아볼 수 있습니다.
이제 원근투영과 소실점을 설명해보도록 하겠습니다.
유클리드 좌표는 앞서 언급했던 것 처럼 각 축들이 서로 영향을 주지 않는다고 설명할 수 있습니다.
그러나 소실점이 있는 사각뿔 형태의 원근 투영 공간은 는 직교하되 는 직교하지 않는 공간이기 때문에 축의 위치에 따라 의 변형이 일어나는 것이라고 수학적 설명이 가능합니다!
그리고 소실점은 투영 평면과 카메라 사이의 거리가 무한대로 길어진다면 결국 투영 평면의 가로와 세로는 0에 수렴하게 된다고 설명을 할 수 있습니다. 이를 다시 정리해보면
초점거리인 를 무한대로 적용하면 투영 화면인 에 수렴하게 된다고 볼 수 있습니다!
지금까지 배웠던 행렬의 종류가 정말 많습니다...
행렬... 이걸 모델행렬로 칭하고... 카메라 위치에 따라서 또 역행렬 적용하고... 이걸 뷰 행렬이라 하고... 이걸 전부 곱하면... 화면 좌표로 최종적으로 나왔던 기억이 납니다.
그런데 이번엔 투영이 들어가서 여기서 또 한번 변환을 해주게 됩니다!
으악...이렇게 작성하니 좀 정신없으니까 한번 과정을 정리해보도록 하겠습니다.
먼저 순서로 곱하여 모델 행렬을 구합니다.
카메라가 바라보는 방향을 로 지정하여 카메라 행렬의 역행렬을 적용하더라도 정상적으로 원하는 뷰행렬을 적용시켜줍니다.
이 단계에선 결국 카메라가 화면에 보여지는 모든 기준이 되기 때문에 역행렬 적용 시 위 이미지와 같이 적용되어 보여집니다.
사각뿔 형태를 통해 원근 투영을 진행합니다. 축을 통해 평면이 변화되므로 투영의 기준을 잡아줄 투영 평면을 지정하고 그 기준에 맞게 투영을 해줍니다.
// CSEngine의 기본 PBR 버텍스 쉐이더의 최종 반환 값
gl_Position = u_projectionMatrix * u_modelViewMatrix * a_position;
위에서 봤던 단계는 하나의 물체를 기준으로 작성하였지만 렌더링에선 결국 여러개가 모인 점들을 위와 같은 단계를 통해 최종적으로 화면에 보여지게 됩니다.
위 코드는 저의 자체엔진에 사용되는 쉐이더의 일부분이며, 실제로 매쉬를 표현하는 버텍스를 위와 같은 식으로 최종처리해주고 있습니다.
이러한 최종변환을 간단하게 식으로 표현하면 다음과 같습니다.
일단 잘 모르겠으니 직접 해보도록 합시다!
앞서 설명한 내용 중에서 투영을 적용할 때 투영의 기준을 잡고 그 기준에 맞게 카메라와의 거리에 따라 평면이 변한다고 적었습니다.
이제 이 투영의 기준을 정하는 방식인 NDC(Normalized Device Coordinate) 좌표를 만들어 투영을 해보도록 하겠습니다!
참고로 앞으로 여기에 작성되는 NDC의 약자는 넥슨 개발자 컨퍼런스가 아니라는 점 명심해주세요!ㅋㅋ
어쨌든 약자에 적힌 대로 투영 평면의 기준을 모두 1인 크기로 노멀라이징해주면 됩니다!
이제 한번 유도해보도록 합시다.
위 이미지에서 초점거리인 를 현재 가지고 있는 정보로 얻어와야합니다.
시야각인 와 정규화된 화면의 길이를 가지고 탄젠트를 통해 유도할 수 있습니다.
직각 삼각형을 통해 구하므로 의 절반을 가지고 탄젠트를 이용해 아래와 같이 식을 만들 수 있습니다.
이제 NDC와 카메라의 거리를 알아냈으니 실 데이터의 좌표를 투영하여 값을 유도하도록 합시다.
여기서 의 을 구하기 위해 아래와 같이 삼각비를 응용하여 구할 수 있습니다.
💡 위 이미지는 다른 의미로 이해를 돕기위해 축이 카메라에서 바라보는 방향으로 나있습니다. 하지만 위 식은 뷰공간 기준으로 이미 이기 때문에 위 이미지와 다르게 음수로 적용한 점 양해바랍니다!
어쨌든 위 식을 풀어보면
축도 축과 직교하기 때문에 똑같이 적용이 가능합니다. 따라서 의 값은 아래와 같이 정리할 수 있습니다.
현재 NDC는 가로세로가 2인 정사각형의 모습을 하고 있습니다.
어? NDC는 크기를 1로 정규화하지 않았나요?
중심인 을 기준으로 크기가 1씩 뻗어나가는 형식이라 최종적인 가로 세로 길이는 2가 됩니다.
하지만 저희가 보고있는 화면은 1920x1080 또는 4k, 심지어 창모드로 본다면 더 다양한 크기와 비율로 확인하게 됩니다!
따라서 지금까지 구했던 NDC를 실제 보여질 화면에 맞게 늘려야합니다. 그런데...
NDC를 그대로 16:9로 늘리게 된 화면입니다. |
그대로 늘려버리니 원래 형태에 비해 많이 찌그러졌습니다! 다시 생각해본다면 당연한 결과입니다.
정사각형으로 그려진 화면을 억지로 늘려버리니 찌그러지게 된겁니다!
NDC단계에서 아예 압축시켜 비율이 맞도록 설정하는 원리 | 해당 원리의 결과물 |
따라서 NDC의 초기설정을 화면 비율에 맞게 반대로 압축시켜서 적용하게됩니다.
이를 종횡비라고 부르며, 식은 아래와 같습니다.
위 식은 세로 기준으로 가로의 비율을 나타내기 때문에 종횡비 의 역수를 에만 적용합니다.
드디어 최종적인 원근 투영식을 얻었습니다! 이제 행렬로 적용하기위해 아래와 같이 만들어줍니다.
와 끝났다! 짝짝짝! 이제 실적용을 해봅시다!
// CSEngine의 기본 PBR 버텍스 쉐이더의 최종 반환 값
gl_Position = u_projectionMatrix * u_modelViewMatrix * a_position;
이제 적용해보려고 했는데... 쉐이더에 막상 적용하려고 보니 뭔가 이상합니다.
분명 포지션값 따로 행렬 따로 적용해야 행렬로 계산하는 이유가 생기는데..
투영행렬은 행렬이 포지션 데이터값이 있어야 완성이 됩니다!
그렇다면 점을 계산할때 마다 투영 행렬을 새로 만들어서 넣어줘야하니...
이거 행렬로 만드는게 의미가 없어집니다!
음.. 뭔가 식 유도는 멀쩡하게 하긴 했지만 행렬이 버텍스 데이터가 필요한 부분에서
실사용으로는 크게 무리가 있어보입니다.
그래도 걱정마세요! 동차 좌표계에서 힌트를 얻을 수 있습니다!
동차 좌표계! 이게 뭘까요? 라는 질문에 저는 차마 대답을 하지 못했습니다...흑흑...
왜냐하면 새로운 축 하나가 추가된다는건 알고 있지만... 그게 아는 것의 끝이기 때문이였습니다.
여러분들은 이 글을 통해 동차의 개념을 이해하고 저보다 더 설명을 잘 해주셨으면 좋겠습니다..!
어쨌든 다시 본론으로 돌아가서, 일단 동차는 우리가 사용하던 차원이 전부가 아닌 더욱 높은 차원이 존재하고 있었고, 이 곳에 영향을 미치고 있다는 개념이라고 보면 좋을 것 같습니다.
찰리 채플린의 명언 중 하나인 는 동차 좌표계를 간단하게 설명하면 이런 느낌이지 않을까 생각이 들었습니다ㅋㅋ
왜냐하면 현 차원에서 해결할 수 없는 문제들을 더 높은 차원에서 해결하도록 유도할 수 있기 때문이죠!
뭔가 닮지 않았나요? 아닌가...?
어쨌든 동차의 성질을 간단한 예시를 통해 알아봅시다.
예시는 직선의 방정식에 동차를 먹여보도록 하겠습니다.
한차원이 추가되기 때문에 기존 를 인 로 표현하겠습니다.
💡 2차원에서 모두 표현하기 때문에 값이 어떻게 될지 사실 상관이 없습니다.
따라서 1로 설정하고 아래의 식처럼 표현이 가능합니다.
따라서 각 좌표는 아래와 같은 식으로 표현될 수 있습니다.
직선의 방정식인 에 동차를 적용해보겠습니다.
여기서 만 남긴다면 아래와 같은 식이 됩니다.
이렇게 상수항없이 모든 항이 동일한 차수를 가지게 되었습니다!
이것을 바로 동차 방정식이라고 합니다!
이제 이러한 동차의 성질을 NDC에 약간 먹여보도록 하겠습니다.
다시 NDC 좌표의 문제점으로 돌아가서 차원을 한단계 올려 다시 정리해줍니다.
여기서 에 해당하는 를 제거하기 위해 아래와 같이 변환해줍니다.
오! 마침내 의 데이터가 사라지고 1이 되었습니다! 행복한 마음으로 이를 행렬로 변환하겠습니다.
햇갈리지 마세요!
동차좌표계를 이용하여 1단계 고차원적인 면에서 계산한 건 맞지만 특별한 경우가 아니라면 실질적인 수식으로서의 가치는 현재 차원의 값입니다. 위 행렬식도 로 이루어져있지만 실질적인 수식으로서의 가치는 에 있기 때문에 3차원의 표현이 아닌 2차원의 표현을 위한 행렬이 되겠습니다.
드디어 온전히 독립된 형태의 투영 행렬을 구했습니다! 이 행렬이라면 바로 쓸 수 있을 것 같습니다!
이렇게 기존 NDC에서 한단계 높은 차원에 의해 독립된 또 다른 행렬을 가진 공간을 클립공간이라고 합니다.
독립된 행렬로 행렬식을 유도했으니 다음엔 한차원을 높인 3차원 NDC에 대해 설명하도록 하겠습니다.