[고도엔진 내부 쉐이더 공부] 스크린 좌표계를 이해하고 텍스처 UV 를 활용해 텍스처 씌우기 - Whitmem
[고도엔진 내부 쉐이더 공부] 스크린 좌표계를 이해하고 텍스처 UV 를 활용해 텍스처 씌우기
Game Development
2025-01-20 19:31 게시 bf644ec02e9ed703a8d7

0
0
89
이 페이지는 외부 공간에 무단 복제할 수 없으며 오직 있는 그대로 게시되며 부정확한 내용을 포함할 수 있습니다. 법률이 허용하는 한 가이드 라인에 맞춰 게시 내용을 인용하거나 출처로 표기할 수 있습니다.
This page is not to be distributed to external services; it is provided as is and may contain inaccuracies.
스크린 좌표계, UV 정점, 화면 좌표계의 이해
이전 게시글에서는 VERTEX 좌표 영역에 대해서 간단하게 이해를 해보았다. VERTEX는 객체의 로컬 좌표를 넣는 공간이고, 엔진에서 이미지 텍스처를 넣은 SPRITE2D의 경우 로컬 좌표계 기준 좌측 위 정점이 (- image width2 , - image height2 ) 가 되고, 우측 아래의 정점이 (+ image width2 , + image height2 ) 가 된다. 즉 이미지 크기가 50px, 50px 인 경우, 좌측 -25px, 우측 25px 상 -25px, 하 25px 를 가지는 4개의 꼭짓점 정점이 만들어지는 것이다. 이 정점이 비로소 월드 행렬을 거쳐 이 세상의 어딘가에 배치되는 것이라고 보면된다.
즉 아래 이미지에서, 주황색 테두리를 가지는 각 꼭짓점 영역은 월드 행렬에 의해 크기가 변환된 것이다. Sprite2D 객체 자체는 이미지 크기에 맞춰서 좌표계의 중심 (0,0)에 고정된 형태를 가진다. 그런데 월드 공간에서 이리저리 움직이고 크기를 조절해서 아래와 같은 주황선의 크기, 이동 정보를 가진다. 출력되는 화면 영역의 SCREEN 좌표를 알아내기 위해서, 로컬 좌표에 MODEL_MATRIX를 곱하고, 카메라의 위치를 반영하는 CANVAS_MATRIX를 곱하고 마지막으로 SCREEN_MATRIX를 곱하면 비로소 현재 로컬 좌표계 및 월드 좌표계에 배치된 정점이 게임 창 화면의 어디에 배치된지 알아낼 수 있다고 하였다.
위 코드는 테스트를 위해서 VERTEX 의 정점에 대해 x가 1.0 이상이고, y가 0.5 이하인 정점들에 대해 X, Y를 50씩 더하는 코드를 작성하였다. Sprite 이미지가 50px, 50px라고 가정한다면, 로컬 좌표계 기준으로 X, Y는 -25 ~ 25 의 범위를 가지기 때문에 위 코드는 x가 25이고, y가 -25인 정점 (25, -25) 우측 위 정점에 대해서 +50 연산이 수행될 것이다. 따라서 우측위 정점에 대해서만 위치가 이상하게 움직여 사각형이 된 것을 볼 수 있다.
또 위 코드에서 새로운 문법이 등장한다. 바로 varying 이다. 원래 Varynig은 vetex() 공간에서 사용한 변수를 fragment()로 넘기기 위해 전역으로 지정하는 전역 변수라고 보면된다. 다른 쉐이더 언어에서는 vertex()를 작성하는 코드 공간과, fragment()를 작성하는 코드 파일이 따로 나누어져 있어 양쪽다 varying 문법을 작성해야 하지만, 이 엔진은 한 파일 안에서 vertex()와 fragment()를 작성하기에 한번만 varying 변수를 선언해주면 되는 것으로 보인다.
즉 위 의도는 vertex()에서 VERTEX 로컬 좌표를 읽어 월드 공간으로 옮기고, 카메라 공간으로 옮기고, 최종적으로 스크린 공간으로 옮겨서 pos 라는 전역 변수에 담는다. 그러면 최종적으로 pos 라는 전역 변수에는 스크린 좌표계 기준 좌측위, 스크린 좌표계 기준 우측위, 스크린 좌표계 기준 좌측 밑, 스크린 좌표계 기준 우측 밑의 정점 데이터로 담겨 픽셀 쉐이더로 넘어간다.
다음은 fragment(픽셀 쉐이더)이다. 현재 색칠하는 픽셀 쉐이더의 좌표를 임의로 색상으로 바꾸어 칠해보고 있다. 그래픽 쉐이더에서는 디버깅이 쉽지 않기 때문에 이렇게 색상으로 출력하여 값이 어떻게 나오는지 예측하고 처리해야 한다. 예를 들어 이 색상에 해당하는 스크린 좌표가 (-0.4, -0.8) 일 때 이 값을 콘솔에 출력할 방법이 없거나 쉽지 않다. 왜냐하면 그래픽은 각각의 픽셀에 대해 독립적으로 실행되는 병렬 연산이기 때문이고, 여기서는 색상 처리만을 수행할 수 있기 때문이다. 따라서 이 좌표 영역을 색상을 의미 하는 R, G, B 값 0 ~ 1로 칠해서 내보내야 한다. 그리고 우리는 비로소 시각적으로 이 색상이 원본 좌표로 어떻게 해독 될 것인지 역으로 해석하여 판단할 수 있다.
예를 들어, 위 소스코드에서 두 개의 COLOR 코드는 본질적으로 동일하다.
FRAGCOORD는 엔진 쉐이더에서 자체 제공하는 스크린 픽셀 좌표이다. 헷갈리면 안된다. 스크린 좌표계가 아니라 스크린 픽셀 좌표이다. 스크린 좌표는 중앙을 기준으로 0,0 이고 좌측 -x, 우측 +x, 상 -x, 하 +x 방향을 가지며 최대 -1 ~ 1 공간인 클립 공간이라고 보면된다. 픽셀 좌표는 이를 0 ~ 화면 너비, 0 ~ 화면 높이로 변환한 좌표계이다. 단순히 수식 연산을 해주면 다시 스크린 좌표계 영역으로 쉽게 이동할 수 있다.
여기서는 스크린 좌표에 대해서 색상으로 출력해보기 위해서 0~1의 범위를 가지는 좌표 값으로 변환해본다. FRAGCOORD는 현재 그리는 스크린 픽셀 좌표인데, x의 경우 0 ~ 너비 범위를 0 ~ 1로 변환하기 위해 1너비 를 곱하면 된다. 이 값을 담는 내장 변수가 SCREEN_PIXEL_SIZE 이다.
즉 정리하자면, 현재 로컬 좌표에서 연산해서 픽셀 연산으로 들어왔고, 픽셀 연산으로 들어온 시점에는 이미 그래픽이 정점 영역에서 보간 및 행렬 처리를 완료하여 스크린 시점으로 들어온 것이다. 이 시점에서는 VERTEX 영역에서 넣었던 로컬 좌표를 잊고 있는 것이 이해하기 편하다. 이미 로컬 좌표는 모두 스크린 좌표로 투영되어 각 픽셀에 대한 스크린 좌표로 보간, 변환된 것이다. 이 상황에서 예를 들어, FRAGCOORD는 현재 그리는 스크린 픽셀 좌표를 의미, 여기에 SCREEN_PIXEL_SIZE 라는 내장 변수는 (1너비, 1높이) 라는 값을 담고 있는 자체 변수이다. 이를 곱해서 0 ~ 1의 범위로 변환한다. 결과적으로 위 과정은 현재 색칠되는 수많은 정점에 대해 각각 스크린 공간을 기준으로 좌측위를 0으로 하고 너비 높이를 1로 하는, 0 ~ 1이라는 정규화된 수치 범위로 계산하는 과정이다.
한편, 다음 코드에 대해서도 같은 의미를 가진다고 했다.
COLOR = vec4((pos.x+1.0)/2.0, ((pos.y)+1.0)/2.0, 1.0, 1.0);
VERTEX 공간에서 로컬 좌표를 스크린 좌표 공간으로 이동시킨 것이 pos 라는 공간이다. fragment 쉐이더로 넘어온 이상 각 담긴 좌표 정보가 모두 보간되어 각 픽셀마다 pos 정보를 얻을 수 있는 상태라고 보면 된다. 그러면 pos 는 스크린 좌표에서 -1 ~ 1 범위 사이에 해당하는 어느 한 x 좌표, pos.y는 스크린 좌표에서 -1 ~ 1 범위 사이에 해당하는 어느 한 y 좌표를 의미한다. 이 좌표는 결국 -1 ~ 1 이라는 공간을 가지기 때문에, +1 해준 뒤 2로 나누어주면 0 ~ 1 의 범위를 구할 수 있다.
실제로 테스트해보면 위 채워진 색상들은 카메라가 움직일 때 해당 공간 뒤에 색이라도 존재하는 듯 카메라 전체 크기 기준 좌측 위는 파랑, 우측위는 분홍, 우측 아래는 흰색, 왼쪽 아래는 하늘색으로 고정되는 것을 확인할 수 있다. 우리는 결국 스크린 공간에서 제일 좌측 위가 0,0 로 하고 우측 위는 1,0, 우측 아래는 1,1 좌측 아래는 0, 1인 값을 구해서 최종적으로 R,G,B(screenUVX, screenUVy, 1) 로 고정시켜 출력했기 때문이다. 카메라를 왼쪽으로 이동해보자. 객체는 우측으로 이동하지만, 색상은 고스란히 이동하는 것이 아니라 카메라에 마스킹이라도 한 듯 해당 지점의 색상이 표시된다.
이미지에 텍스처 UV 맵핑
사실 이전 섹션은 쉐이더 투영의 기본적인 원리를 익히고 그 과정을 제대로 이해하기 위해 출력해본 것으로 해당 표현을 의도하는게 아닌 이상 당장 큰 필요는 없다. 하지만 고급 기능을 구현하고 제대로된 연출을 하기 위해서는 이러한 변환은 기본으로 이해해야하기에 변환 과정을 자세히 설명한 것이다. 쉐이더를 하다보면 공간의 이동을 수없이 많이 하기 때문에 이 부분을 헷갈리지 않도록 연습해야 한다. 이번에는 텍스처 UV 좌표를 건들어보려고 한다. 텍스처 UV는 생각보다 복잡하지 않다. 위 스크린 좌표는 투영 과정을 거쳐 연산을 해보려고 직접 계산한 것이지, 사실상 스크린 좌표도 원리만 익히면 복잡하지 않다.
다시 돌아와서, Sprite2D에 어떤 텍스처를 넣으면 이 텍스처가 지정된 공간에 늘어지든 작아지든 어떻게든 잘 표시된다.
결국 엔진 기본에 텍스처를 표시하는 쉐이더가 기본 내장되어 있는 것인데, 우리가 이제 직접 이 효과를 조절하고 구현하기 위해서는 텍스처를 쉐이더로 구현하는 건 식은 죽 먹기여야 한다.
지금 이 순간 만큼은 저번 섹션의 내용을 모두 잊어도 좋다. UV 좌표계는 그래픽이 기본적으로 보간 연산을 해주고, 객체에서 제공되기 때문에 가져다 쓰면된다. UV 좌표는 단순히 fragment 쉐이더가 색칠되는 영역안에서 시작점을 (0,0)으로 보고 끝 점을 (1,1) 으로 본 텍스처 좌표계라고 보면 된다.
이때동안 이미지의 특정 색을 가져오기 위해서 이미지 픽셀 좌표 최대 (image_width, image_height) 로 설명하곤 했었는데, 이미지 크기에 따라 이런식으로 계산을 일일이 하려고 하니 복잡해지기에, 이미지 너비 높이를 0 ~ 1로 정규화하여 표현하자는 것이 UV 좌표이다. 따라서 UV 좌표 (0.5, 0.5)는 각 이미지에서의 중앙을 표현하고, (0,0)은 각 이미지에서의 제일 좌측위를 표현하고, (1,1)은 각 이미지에서의 제일 좌측 아래를 표현하는 좌표계이다. 그냥 픽셀 단위를 안쓰고 0 ~ 1로 정규화된 단위를 쓰는 것이다.
심지어, fragment 쉐이더에서는 현재 색칠되는 영역이 현재 객체 기준 어떤 UV 좌표를 가져야하는지도 계산해서 변수로 준다. 즉 아까 알아보았던 스크린 좌표계 처럼 화면 전체 기준의 좌표가 아니라, 현재 그리는 객체에서 정점의 시작점을 (0,0)으로 하고 끝 점을 (1,1)로 하는 데이터를 반환해준다. Sprite2D 객체를 예로 들어, Sprite2D 객체가 어딘가에 배치되어 있다면 그 객체를 그리기 시작하는 지점인 제일 좌측 위 (0,0) 오른쪽 아래 (1,1)로 하여금 보간하여 던져준다. 우리는 이 값을 가져다가 이미지에서 색상을 가져와 표시해주면 되는 것이다.
fragment 쉐이더에서 사용할 수 있는 함수가 하나 있는데, 바로 texture()이라는 함수이다. 첫 번째 인자에 어떤 이미지 텍스처인지와 두 번째 인자에 0~1에 해당하는 좌표를 넣어주면 해당 텍스처의 해당 지점의 색상을 가져와준다. 이 색상을 단순히 COLOR에 반환하면 텍스처를 표시하는 쉐이더가 완성되는 것이다.
fragment 쉐이더에서 자체적으로 사용할 수 있는 UV 를 받는 예약어 변수가 UV이다. 현재 연산하는 픽셀 좌표에 해당하는 UV 좌표를 알아서 던져준다. TEXTURE은 고도 엔진에서 기본적으로 이 Sprite2D에 배치된 Texture 이미지를 의미하는 유닛 넘버인 것으로 보인다. 우리가 임의로 텍스처를 받아서 처리할 수도 있으나, 여기서는 스프라이트에 달려있는 텍스처를 받아와 출력하는 것을 목표로 한다.
Vertex 에서 아무리 객체가 이상하게 휘어있더라도, UV는 알아서 자동으로 보간되기 때문에 늘려져서 표시되는 것을 확인할 수 있다.
헷갈리니까, VERTEX 부분을 아예 비워 원본 버텍스를 유지하도록 하였다. COLOR 값는 텍스처를 가져와 표시(샘플링)하는데, UV 값에 + 0.1을 해 주었다. 이러면 각각 가져오는 UV 값이 모두 0.1씩 더해지기 때문에 원래 이미지에서 가져오려는 영역보다 더 오른쪽 밑을 가져오게 된다. 따라서 객체에서의 텍스처 이미지는 좌측 위로 이동한다.
이로써 텍스처를 샘플링하는 것은 기본적으로 완료되었다. 여기까지 제대로 이해했다면 앞으로의 쉐이더 개발은 큰 장애물은 없을 것으로 보인다. 다음에는 색을 조합하고 여러 텍스처를 가져와 처리하는 방법에 대해서 알아보고자 한다. 그 다음부터는 본격적으로 여러 텍스처 효과를 통해 다양한 효과를 구현해보고자 한다.
댓글 0개
댓글은 일회용 패스워드가 발급되며 사이트 이용 약관에 동의로 간주됩니다.
확인
Whitmemit 개인 일지 블로그는 개인이 운영하는 정보 공유 공간으로 사용자의 민감한 개인 정보를 직접 요구하거나 요청하지 않습니다. 기본적인 사이트 방문시 처리되는 처리 정보에 대해서는 '사이트 처리 방침'을 참고하십시오. 추가적인 기능의 제공을 위하여 쿠키 정보를 사용하고 있습니다. Whitmemit 에서 처리하는 정보는 식별 용도로 사용되며 기타 글꼴 및 폰트 라이브러리에서 쿠키 정보를 사용할 수 있습니다.
이 자료는 모두 필수 자료로 간주되며, 사이트 이용을 하거나, 탐색하는 경우 동의로 간주합니다.