본문 바로가기
코딩/셰이더 프로그래밍 입문

[셰이더 프로그래밍] 03. 텍스쳐매핑

by witn331ss 2023. 5. 31.

텍스처매핑과 UV좌표

앞에서 3D물체를 이루는 구성요소는 삼각형이며

정점 3개로 삼각형을 만들 수 있다고 설명했었다.

 

그렇다면 삼각형 위에 이미지를 입히려면 어떤 방법을 써야할까?

 

먼저 이 삼각형의 왼쪽 꼭짓점에 해당 이미지의 오른쪽 귀퉁이 픽섹을 출력할 것 같은 명령을 내릴 수 있어야 한다.

삼각형은 정점 3개로 이루어져 있기 때문에 각 정점을 텍스쳐 위에 있는 한 픽셀에 대응시켜주면 된다.

 

그럼 텍스쳐 위에서 한 픽셀은 어떻게 가리킬 수 있을까?

텍스쳐란 결국 이미지 파일임으로 X=60 Y=101 에 있는 픽셀이라고 정의하면 될까?

 

만약 이렇게 정의한다면 추후 이미지 파일의 크기를 2배 늘리면 이것을 다시 X=120 Y=202으로 바꿔줘야만 할 것이다.

좋은 방법은 아니다.

 

그렇기때문에 텍스쳐 또한 백분율로 표기하면 된다.

 

버텍스 쉐이더를 살펴보기 전에 텍스쳐맵핑을 하려면 어떤 데이터가 필요할까?

당연하게도 텍스쳐로 사용할 이미지가 필요할 것이다.

 

그렇다면 텍스쳐를 입히는 작업은 어디에서 해야만 하는가?

각 쉐이더가 실행되는 시점을 생각해보면 이에 대한 답을 쉽게 구할 수 있다.

 

텍스쳐는 표면을 구성하는 모든 픽셀에 입혀야 하기 때문에 픽셀쉐이더에서 맵핑을 해야한다.

이미지는 텍스쳐로 사용할 것이니 버텍스 쉐이더에서는 선언할 필요가 없다.

 

그 외에 또 다른 정보가 필요한 것은 바로 UV좌표.

UV좌표는 전역변수가 아닌 정점데이터의 일부로 전달되기에

 

이 점을 염두하며 버텍스쉐이더의 입출력 데이터를 살펴보자.

 

버텍스쉐이터의 입력 데이터

이전에 사용했던 입력 데이터의 구조체를 한번 가져와보겠다.

struct VS_INPUT
{
   float4 mPosition : POSITION;
}

여기에서 UV좌표를 추가해줘야한다. UV같은 경우는 float2를 사용한다.

위치정보가 POSITION 이라는 시맨틱을 가진 것처럼 UV 또한 TEXCOORD라는 시맨틱을 가지고 있다.

UV 좌표를 삽입한 뒤 버텍스쉐이더의 입력 데이터는 다음과 같다

 

struct VS_INPUT
{
   float4 mPosition : POSITION;
   float2 mTexcoord : TEXCOORD0;
};

TEXCOORD 뒤에 0을 붙인 이유는 HLSL에서 지원하는 TEXCOORD 수가 여러개이기 때문이다.

쉐이더에서 여러 개의 텍스쳐를 동시에 사용할 때 둘 이상의 좌표를 사용할 경우가 있는데

 

그럴 때에는 TEXCOORD0  TEXCOORD1 등으로 시맨틱을 사용하면 된다.

 

버텍스쉐이더의 출력 데이터

또 이전에 작성한 출력 데이터를 가져와보자.

struct VS_OUTPUT
{
  float4 mPosition : POSITION;
};

출력데이더에 다른 정보가 필요할까?

 

버텍스쉐이더는 사실 위치정보 외에도 다른 정보들을 반환할 수 있다.

버텍스쉐이더가 위치정보를 반환하는 이유는 래스터라이저가 픽셀들을 찾아낼 수 있게 하는 것이다.

그 외에 다른 정보를 반환하는 이유는 래스터라이저를 위한 것이 아니라

픽셀쉐이더를 위한 것이다. 텍스쳐매핑에 필요한 UV좌표가 그 좋은 예시이다.

 

픽셀 쉐이더는 버텍스 버퍼 데이터에 직접적으로 접근을 못한다.

 

따라서 픽셀쉐이더에서 사용해야할 버텍스 데이터가 있다면

그 데이터는 버텍스쉐이더를 거쳐 픽셀 쉐이더에 전달되어야 한다.

 

조금은 번거로운 제약 같지만 아래 그림을 보면 왜 이런 제약이 붙는지 알 수 있다.

UV좌표가 정의된 장소는 각 정점이다.

하지만 위 그림에서 볼 수 있듯 픽셀의 UV좌표는 버텍스의 UV좌표와도 다른 것이 많다 (픽셀과 버텍스 위치가 일치하는 경우에는 좌표가 같다.)

 

따라서 이 픽셀의 올바른 UV값을 구하는 방법은

 

현재 위치에서 세 정점까지의 거리를 구한 뒤 그 거리의 비율에 따라 세 UV값을 혼합하는 것이다.

 

이 혼합은 직접 해줄 필요는 없으며

버텍스쉐이더에서 출력한 위치정보를 래스터라이저가 알아서 처리해준것처럼

정점 이외의 기타 정보는 보간기(interpolator) 라는 장치가 알아서 혼합해주고있다.

 

보간기가 하는 역할은 UV 좌표 뿐만이 아니다.

버텍스쉐이더가 반환하는 어떤 값이든 보간기는 보간을 해서 픽셀쉐이더에 전달해준다.

 

그렇기때문에 버텍스쉐이더에서 UV좌표값 또한 반환해야한다.

 

struct VS_OUTPUT
{
   float4 mPosition : POSITION;
   float2 mTexCoord : TEXCOORD0;
};

 

 

버텍스 쉐이더 함수

버텍스쉐이더의 가장 중요한 임무는 버텍스의 위치를 투영공간으로 변환시키는 것이다.

VS_OUTPUT vs_main(vs_INPUT Input)
{
  VS_OUTPUT Output;

  Output.mPosition = mul(Input.mPosition, gWorldMatrix);
  Output.mPosition = mul(Output.mPosition, gViewMatrix)'
  Output.mPosition = mul(Output.mPosition, gProjectionMatrix);
}

이제 UV좌표를 전달할 차례인데

Output구조체에서 UV좌표를 대입하기 전에 공간변환을 적용해야 할까?

 

답은 아니다. UV좌표는 지금까지 다뤘던 3차원 공간에 존재하는 것이 아니라

삼각형의 표면상에 존재하기에 아무 변환 없이 UV좌표를 전달해준다.

 

Output.mTexCoord = Input.mTexcoord;

이 이상 처리할 데이터가 없으니 output을 반환하면서 함수를 마무리 짓는다.

return Output;

 

픽셀 쉐이더

픽셀쉐이더에서 할일은 텍스처이미지에서 텍셀을 구해와 색을 화면에 출력하는 것.

그렇다면 텍스쳐로 사용할 이미지와 현재 픽셀의 UV좌표가 필요하다.

 

텍스쳐 이미지는 픽셀마다 변하는 값이 아닌 전역변수로

UV좌표는 버텍스쉐이더로부터 보간기를 거쳐 들어온 입력데이터가 된다.

 

우선 픽셀 쉐이더의 입력 데이터 구조체부터 만들어보자.

struct PS_INPUT
{
   float2 mTexCoord : TEXCOORD0;
};

VS_OUTPUT 구조체를 가져와 mPosition을 지워버린 것 뿐이다.

사실 픽셀쉐이더의 입력 데이터는 버텍스쉐이더의 출력 데이터와 일치할 수 밖에 없는데

 

버텍스쉐이더에서 반환한 값을 가져오는것이기 때문이다.

 

다음은 텍스쳐를 선언할 차례인데

앞서 렌더몽키 프로젝트를 설정할 때 DiffuseSampler 라는 이름의 텍스쳐 개체를 만들었었다,

 

바로 이 개체가 텍셀을 구할 때 사용하는 텍스처 샘플러이다.

따라서 HLSL코드에서 사용하는 텍스쳐 샘플러의 이름도 DiffuseSampler이여야 한다.

 

sampler2D DiffuseSampler;

smapler2D는 HLSL에서 지원하는 데이터형 중 하나로

2D텍스쳐에서 텍셀 하나를 구해오는데 사용된다.

 

 

픽셀쉐이더 함수

float4 ps_main (PS_INPUT Input) : COLOR

우선 헤더부터 살펴보자.

 

이전과 달라진 점은 보간기가 계산해준 UV좌표를 가져오기 위해서

PS_INPUT형의 Input 매개변수를 받는다는 것 뿐이다.

 

이제 UV값과 텍스쳐 샘플러가 있으므로 텍셀 값을 구하는 일만 남았다.

 

HLSL내장 함수 tex2D를 사용하면 매우 쉽게 이런 일을 할 수 있다.

tex2D는 첫번째 매개변수로 텍스쳐 샘플러를 부 번째 매개변수로 UV좌표를 받는다.

 

float4 albedo = tex2D(DiffuseSampler, Input.mTexCoord);

이 코드는 Diffusesampler에서 Input.mTexcoord 좌표에 있는 텍셀을 읽어와서

그 값을 albedo 변수에 저장한다.

 

우리는 텍스쳐를 그대로 보여주는게 목적임으로 그냥 반환하면 된다.

 

 

근데 이대로 컴파일하고 미리보기 창을 보면 엉말이다

왜냐하면 버텍스 버퍼에서 올바른 UV좌표 값을 불러오도록 설정하지 않았기 때문이다.

 

Workspace 패널에서 Stream mapping 을 찾아 더블클릭하고

새항목을 추가해주자.