이 장에서 새로 배우는 HLSL
- sampler2D - 텍스처에서 텍셀을 구해올 때 사용하는 샘플러 데이터형
- tex2D() - 텍스처 샘플링에 사용하는 HLSL 함수
- 스위즐(swizzle) - 벡터 성분의 순서를 마음대로 뒤섞을 수 있는 방법
저번 장에서 배운 내용 어떠셨나요? 너무 쉬었다고요? 실제 게임에서 별 쓸모가 없어 보인다고요? 네, 사실 그렇습니다. 저번 장의 주 목적은 실습을 통해 HLSL의 기초 문법을 배우는 것이었습니다. 보통 프로그래밍 책에서 헬로월드(hello world) 예제를 처음에 드는 것과 마찬가지 이치죠. 그럼 이번 장에서는 그보다 조금 더 쓸모가 있는 내용을 배워볼까요? 물체를 단색으로 출력하는 대신에 표면에 텍스처(texture)를 입혀보는 게 어떨까요? 이걸 보통 텍스처매핑(texture mapping)이라고 부른다는 것쯤은 다 아시죠? 1
텍스처매핑과 UV 좌표
3D 물체를 이루는 구성요소는 삼각형이라고 이전에 말씀드렸습니다. 정점 3개로 삼각형을 만들 수 있다는 것도요. 그렇다면 삼각형 위에 이미지를 입히려면 어떻게 해야 할까요? '이 삼각형의 왼쪽 꼭짓점에 저 이미지의 오른쪽 귀퉁이 픽셀을 출력할 것'과 같은 지시를 내릴 수 있어야겠죠? 삼각형은 이미 정점 3개로 이루어져 있으니 각 정점을 텍스처 위에 있는 한 픽셀에 대응시켜 주면 되겠군요. 그럼 텍스처 위에서 한 픽셀을 어떻게 가리킬까요? 텍스처란 결국 이미지 파일이니까 'x = 30, y = 101에 있는 픽셀'이라는 식으로 정의하면 될까요? 만약 이렇게 정의를 해버리면 나중에 이미지 파일의 크기를 2배로 늘리면 이것을 다시 x = 60, y = 202로 바꿔야겠네요. 별로 바람직하지 않죠? 2
저번 장에서 배운 내용을 떠올려보니 색상을 표현할 때도 비슷한 이야길 했던 것 같군요. 그 때, 채널의 비트 수에 상관없이 통일적으로 색상을 표현하려면 어떻게 해야 한다고 했죠? 모든 값을 백분율(0~1)로 표현한다고 했죠? 여기서도 똑같은 방법을 사용합니다. x = 0이 텍스처의 제일 왼쪽 열을, x = 1은 제일 오른쪽 열을 나타낸다고 하면 되겠죠? 마찬가지로 y = 0은 텍스처의 제일 처음 행을, y = 1은 마지막 행을 나타냅니다. 참고로 텍스처매핑을 사용할 때는 XY대신에 UV를 사용하는 게 보통입니다. 특별한 이유는 없고 그냥 위치를 표현할 때 흔히 xy를 사용하니 그와 혼돈을 피하기 위해서 입니다. 이것을 그림으로 표현하면 다음과 같습니다.
그림 3.1. 텍스처의 UV 좌표
이제 다양한 UV 좌표를 대입하면 어떻게 결과가 달라지는지는 몇 가지 예를 들어보도록 하죠. 역시 그림으로 보면 이해가 쉽겠죠?
그림 3.2 다양한 텍스처매핑의 예
(a) 아직 텍스처를 입히지 않은 두 삼각형입니다. 정점 v0, v1, v2와 v0, v2, v3가 각각 삼각형을 하나씩 이루고 있군요.
(b) UV 좌표의 범위가 (0,0) ~ (1,1)입니다. 텍스처를 전부 다 보여줍니다.
(c) UV 좌표의 범위가 (0,0) ~ (0.5, 1)입니다. 따라서 텍스처의 왼쪽 절반만을 모여줍니다. 0.5가 백분율로는 50%니까 딱 중간인 거 맞죠?
(d) UV 좌표의 범위가 (0,0) ~ (0.5, 0.5) 이군요. 따라서 이미지의 왼쪽 절반과 위쪽 절반만을 보여줍니다.
(f) UV 좌표의 범위가 (0,0) ~ (2,2)니까 텍스처를 위아래로 두 번, 그리고 좌우로 두 번 반복해줍니다.란 시맨틱이 바로 그것입니다. UV 좌표 데이터를 삽입한 뒤의 정점쉐이더 입력데이터는 아래와 같습니다. 4
struct VS_INPUT
{
float4 mPosition : POSITION;
float2 mTexCoord : TEXCOORD0;
};
TEXCOORD뒤에 0을 붙인 이유는 HLSL에서 지원하는 TEXCOORD 수가 여럿이기 때문입니다. 쉐이더에서 여러 개의 텍스처를 동시에 사용할 때, 둘 이상의 UV 좌표를 사용할 경우가 있는데 그럴 때에는 TEXCOORD0, TEXCOORD1등으로 시맨틱을 사용하시면 됩니다.
정점쉐이더 출력데이터
우선 '제2장: 진짜 쉬운 빨강쉐이더'에서 사용했던 정점쉐이더 출력데이터를 가져와 봅시다.
struct VS_OUTPUT
{
float4 mPosition : POSITION;
};
여기에 다른 정보를 추가해야 할까요? '제2장: 진짜 쉬운 빨강쉐이더'에서 설명해 드리지 않았던 내용 중 하나가 정점쉐이더는 위치정보 외에도 다른 정보들을 반환할 수 있다는 것입니다. 정점쉐이더가 위치정보를 반환하는 이유는 래스터라이저가 픽셀들을 찾아낼 수 있도록 하기 위해서였습니다. 하지만, 위치 이외의 다른 정보를 반환하는 이유는 래스터라이저를 위해서가 아닙니다. 이는 오히려 픽셀쉐이더를 위해서입니다. 텍스처매핑에 필요한 UV 좌표가 그 좋은 예입니다.
픽셀쉐이더는 정점 버퍼 데이터에 직접적으로 접근을 못 합니다. 따라서, 픽셀쉐이더에서 사용해야 할 정점데이터가 있다면(예, UV 좌표), 그 데이터는 정점쉐이더를 거쳐 픽셀쉐이더에 전달돼야 합니다. 좀 쓸데없는 제약 같다고요? 다음의 그림을 보시면 왜 이런 제약이 붙어 있는지를 알 수 있으실 것입니다.
그림 3.4. 과연 저 픽셀의 UV 좌표 값은 무엇일까?
UV 좌표가 정의된 장소는 각 정점입니다. 하지만 위 그림에서 볼 수 있듯이 픽셀의 UV 좌표는 정점의 UV 좌표와도 다른 것이 대부분입니다. 따라서 이 픽셀의 올바른 UV 값을 구하는 방법은 현재 위치에서 세 정점까지의 거리를 구한 뒤 그 거리의 비율에 따라 세 UV 값을 혼합하는 것이겠지요. 하지만 이런 혼합을 직접해줄 필요는 없습니다. 정점쉐이더에서 출력한 위치 정보를 래스터라이저가 알아서 처리해줬듯이 정점 이외의 기타 정보는 보간기(interpolator)라는 장치가 알아서 혼합해줍니다. 그럼 '제1장: 쉐이더란 무엇이죠?'에서 보여드렸던 GPU 파이프라인에 보간기를 추가해보죠. 그림 3.5가 되겠습니다. 5
그림 3.5. 보간기까지 추가했지만 여전히 너무 간략한 3D 파이프라인
자, 그럼 이제 정점쉐이더에서 UV 좌표값도 반환해야 한다는 사실, 이해하시겠죠? 추가합시다.
struct VS_OUTPUT
{
float4 mPosition : POSITION;
float2 mTexCoord : TEXCOORD0;
};
전역변수
'제2장: 진짜 쉬운 빨강쉐이더'에서 사용했던 것 이외에 별도로 필요한 전역변수는 없습니다. 따라서 별다른 설명 없이 코드만 보여 드리겠습니다.
float4x4 gWorldMatrix;
float4x4 gViewMatrix;
float4x4 gProjectionMatrix;
정점쉐이더 함수
누누이 말씀드리지만 정점쉐이더의 가장 중요한 임무는 정점의 위치를 투영공간으로 변환시키는 것입니다. 이 코드는 '제2장: 진짜 쉬운 빨강쉐이더'의 쉐이더에서 사용했던 것과 똑같습니다.
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;
}
p.s. 여기는 댓글 달릴때마다 이메일이 오지 않아서 답변을 빨리빨리 드리기가 쉽지 않군요. 제 개인 블로그에 포스팅 된 버전에 댓글을 다시는게 제일 답변이 빠릅니다. 오탈자 및 잘못된 내용 수정도 제일 빠릅니다.
이 외에도 UV 좌표의 범위를 (1,0) ~ (0,1)로 하면 텍스처의 좌우를 뒤집는 등의 효과도 줄 수 있습니다. 이 정도면 어떻게 UV 좌표를 지정해야 원하는 결과를 얻을 수 있는 지 대충 아시겠죠? 이 정도면 실제로 텍스처매핑 쉐이더를 작성할 준비가 된 것 같군요.
기초설정
- '제2장: 진짜 쉬운 빨강쉐이더'에서 했던 것과 마찬가지로 렌더몽키 안에서 새로운 DirectX 이펙트를 만든 뒤, 정점쉐이더와 픽셀쉐이더 코드를 삭제합니다.
- 이제 쉐이더의 이름을 TextureMapping으로 바꿉니다.
- 정점의 위치를 변환할 때 사용할 gWorldMatrix, gViewMatrix, gProjectionMatrix를 추가하는 것도 잊지 맙시다. 변수 시맨틱을 이용해서 실제 데이터를 전달해 주는 방법도 기억하시죠?
- 이제 텍스처로 사용할 이미지를 추가할 차례입니다. TextureMapping 쉐이더에 오른쪽 마우스 버튼을 누른 뒤, Add Texture > Add 2D Texture > 렌더몽키 설치폴더\examples\media\textures\earth.jpg 파일을 선택합니다. Earth라는 이름의 텍스처가 추가되었을 겁니다.
- 이 텍스처의 이름을 DiffuseMap으로 변경합니다.
- 이제 Pass 0위에 마우스 오른쪽 버튼을 누른 뒤, Add Texture Object > DiffuseMap을 선택합니다. Texture0 이란 이름의 텍스처 개체가 추가되었죠?
- 이제 Texture0의 이름을 DiffuseSampler로 바꿉니다.
- 이 모든 설정을 마치셨다면 Workspace 패널이 다음 그림처럼 보일 겁니다.
그림 3.3. 기초설정을 마친 렌더몽키 프로젝트
정점쉐이더
일단 전체 소스코드부터 보여드린 뒤, 한 줄씩 차근차근 설명해드리겠습니다.
struct VS_INPUT
{
float4 mPosition : POSITION;
float2 mTexCoord : TEXCOORD0;
};
struct VS_OUTPUT
{
float4 mPosition : POSITION;
float2 mTexCoord : TEXCOORD0;
};
float4x4 gWorldMatrix;
float4x4 gViewMatrix;
float4x4 gProjectionMatrix;
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);
Output.mTexCoord = Input.mTexCoord;
return Output;
}
정점쉐이더를 살펴보기 전에 텍스처매핑을 하려면 어떤 데이터가 새로 필요한지 생각해봅시다. 일단 당연히 텍스처로 사용할 이미지 하나가 필요하겠죠? 그렇다면 텍스처를 입히는 작업을 어디에서 해야 할까요? 정점쉐이더일까요? 아니면 픽셀쉐이더일까요? 각 쉐이더가 실행되는 시점을 생각해보면 이에 대한 대답을 쉽게 구할 수 있습니다. 정점쉐이더는 각 정점마다 실행이 된다고 말씀드렸었죠? 근데 텍스처는 어디에 입히죠? 정점에 입히는 게 아니라 표면을 구성하는 모든 픽셀에 입혀야 하죠? 따라서 정점쉐이더에서 하기엔 뭔가 부족할 듯 싶군요. 정점쉐이더와는 달리 픽셀쉐이더는 각 픽셀마다 호출이 되니까 당연히 픽셀쉐이더에서 텍스처매핑을 해야겠군요. 자, 그럼 이미지는 텍스처로 사용할 테니 정점쉐이더에서 선언해줄 필요가 없네요.
그럼 이 외에 다른 정보가 필요할까요? 바로 위에서 말씀드렸었는데 말이죠. 그렇습니다. UV 좌표가 필요하지요. UV 좌표를 어디에 지정했었죠? 각 정점마다였죠? 따라서 UV 좌표는 전역변수가 아니라 정점데이터의 일부로 전달됩니다. 자~ 그럼 이 점을 염두에 두고 정점쉐이더의 입출력 데이터를 살펴보도록 합시다.
정점쉐이더 입력데이터
'제2장: 진짜 쉬운 빨강쉐이더'에서 사용했던 입력데이터의 구조체를 일단 가져와보도록 하지요.
struct VS_INPUT
{
float4 mPosition : POSITION;
};
자, 이제 여기에 UV 좌표를 추가해야겠죠? UV 좌표는 u하고 v로 나뉘니까 데이터형은 float2가 되겠네요. 그렇다면 어떤 시맨틱을 사용해야 할까요? 위치정보가 POSITION이라는 시맨틱을 가졌듯이 UV 좌표도 자신만의 시맨틱을 가지겠죠? TEXCOORD[footnote]텍스처좌표(texture coordinate)의 줄임말입니다. [본문으로]
픽셀의 위치가 정점과 일치하는 경우에는 UV 좌표가 같습니다. [본문으로]
보간(interpolate)이란 단어가 잘 이해 안 되시는 분들은 그냥 위에서 설명해 드렸다시피 '인접한 세 정점까지의 거리에 비례하여 값을 혼합하는 것'이라고 이해하세요. [본문으로]
'Shader' 카테고리의 다른 글
[포프의 쉐이더 입문강좌] 04. 기초적인 조명쉐이더 Part 1 (4) | 2011.12.26 |
---|---|
[포프의 쉐이더 입문강좌] 03. 텍스처매핑 Part 2 (0) | 2011.12.19 |
[포프의 쉐이더 입문강좌] 02. 진짜 쉬운 빨강쉐이더 Part 2 (15) | 2011.12.06 |
[포프의 쉐이더 입문강좌] 02. 진짜 쉬운 빨강쉐이더 Part 1 (3) | 2011.12.06 |
[포프의 쉐이더 입문강좌] 01. 쉐이더란 무엇이죠? Part 2 (14) | 2011.12.05 |