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

[셰이더 프로그래밍] 02. 진짜 쉬운 빨강쉐이더

by witn331ss 2023. 5. 31.

렌더몽키에서 빨강색 공을 그리는 셰이더를 작성하고

그 결과를 .fx파일로 엑스포트 한 뒤 이걸 DirectX 프레임워크에 그대로 가져다가 사용할 것이다.

 

이렇게 기초설정을 해주자.
코드는 이 창에 들어가면 확인할 수 있다.

struct VS_INPUT
{
   float4 mPosition : POSITION;
};

struct VS_OUTPUT
{
   float4 mPosition : POSITION;
};

float4x4 gWorldMatrix;
float4x4 gViewMaatrix;
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);
}

 

전역변수 VS 정점 데이터

셰이더에서 사용할 수 있는 입력 값으로는 전역변수와 정점데이터가 존재한다.

이 둘을 구분짓는 기준은 '한 물체를 구성하는 모든 정점이 동일한 값을 사용하느냐?' 의 여부이다.

 

만약 동일한 값을 사용한다면 이것은 전역변수가 될 수 있지만

각 정점마다 다른 값을 사용한다면 이것은 전역변수가 될 수 없다.

 

그 대신 정점버퍼, 그러니까 정점 데이터의 일부로 이 값을 받아들여야만 한다.

 

전역변수의 예시로는 월드행렬, 카메라의 위치 등이 있고

정점데이터 변수의 예는 정점의 위치, UV좌표등이 있다.

 

정점셰이더의 입력 데이터

정점셰이더에서 입력받은 데이터를 VS_INPUT 구조체로 선언할 것이다.

 

struct VS_INPUT

{
   float4 mPosition : POSITION;
}

정점 셰이더의 가장 중요한 임무는 각 정점의 위치를 공간변환하는 것이라고 했었다.

그러기 위해서 정점의 위치를 입력받아야 한다.

 

바로 앞 구조체가 맴버변수 mPosition을 통해 정점 위치를 받아온다.

 

이 변수가 DirectX의 정점버퍼(버텍스 버퍼라고도 한다) 로부터 위치 정보를 구해올 수 있는 이유는

POSITION이라는 시맨틱(의미를 부여한 태그) 때문이다.

 

정점버퍼에서는 정점의 위치, UV 좌표, 법선(normal)등을 비롯한 다양한 정보가 담겨있을 수 있는데

이 중에서 필요한 것만 빼오는 것을 시맨틱이라고 한다.

 

따라서

float4 mPosition : POSITION;

이것은 정점 데이터에서 위치 정보를 가져와 mPosition에 대입하라는 명령이다.

 

float4 는 변수의 데이터형으로 XYZW 좌표를 가진 부동소수점형이다.

 

 

##

 

정점셰이더의 출력 데이터

 

정점셰이더의 입력 데이터를 선언했으므로 이제 출력데이터를 살펴볼 차례인데

GPU 파이프라인을 간단히 생각해보자. 각 픽셀의 위치를 찾아내려면 

 

정점 셰이더가 위치변환 결과를 래스터라이저에게 전달해줘야만 한다.

 

따라서 정점셰이더는 반드시 위치변환 결과를 반환해야만 하는데

정점 셰이더의 출력데이터 구조체를 VS_OUTPUT으로 선언해보겠다.

 

struct VS_OUTPUT
{
    float4 mPosition : POSITION;
};

float4 형으로 위치 데이터를 반환하면서 이건 위치다! 라고 시맨틱이 붙었다.

 

 

##

 

전역변수

정점셰이더에서 공간변환을 할 떄 사용해야하는 전역변수가 몇 개 있는데

그 전에 공간변환이 무엇인지부터 먼저 살펴보자.

 

 

3D 공간변환

3D 물체를 모니터에 그리려면 정점들의 위치를 공간변환해야만 한다고 했었다.

그렇다면 어떤 공간을 거쳐야 3D물체를 모니터에 보여줄 수 있을까?

 

 

물체공간

 

사과를 예로 들어보겠다.

 

사과의 중앙을 원점으로 삼고 그 점을 시작으로 가상의 XYZ축을 만들어보자.

이제 원점으로부터 사과의 표면까지의 거리를 재어보면 각 점들을 XYZ좌표라고 할 수 있겠다.

 

이 정점들을 묶어 삼각형들을 만들면 폴리곤으로 사과모델을 만들 수 있다.

 

이 사과를 들어 이리저리 움직여봐도

원점으로부터 각 정점까지의 거리는 변하지 않는다.

 

이것이 바로 물체공간(object space) 또는 지역공간 (local space) 인 것이다.

 

물체공간에서는 각 물체(obj)가 자신만의 좌표계를 가짐으로

다수의 물체를 통일적으로 처리하기 어렵다.

 

 

월드공간

이제 사과를 모니터 앞에 놓아본다고 생각해보자.

모니터 또한 본인만의 물체공간을 가지고 있다.

 

이 둘을 통일적으로 처리하고 싶은데 어떤 방법을 사용해야 할까?

 

답은 이 두 물체를 같은 공간으로 옮겨오면 된다.

그러려면 공간 하나를 새롭게 만들어야 하는데.

 

현재 있는 방의 입구를 원점으로 삼고

이제 그 원점에서부터 모니터를 구성하는 정점들까지의 거리를 잰다면

새로운 XYZ좌표로 나타낼 수 있을 것이다.

 

사과 또한 같은 방법으로 표현할 수 있는데 이 새로운 공간을 월드공간 이라고 한다.

 

 

뷰공간

 

그렇다면 이제 카메라를 가져가서 이 모니터와 사과의 사진을 찍는다고 생각해보자.

일단 위 두 물체들이 모두 사진에 들어오도록 사진을 찍고 , 다음에는 두개가 보이지 않는 엉뚱한 곳을 찍는다고 가정해보자.

 

첫번째에서는 두 물체를 볼 수 있는데

두번째 사진에서는 사과나 모니터의 흔적도 찾아볼 수 없다.

 

그렇다면 이 두 사진간 위치변화가 있어야 한단 소리인데

두 물체의 위치는 전혀 변하지 않았다.

 

그걸 생각해본다면 카메라 좌표도 다른 공간을 사용하고 있다는 것을 알 수 있을 것이다.

이렇게 카메라가 사용하는 공간을 뷰공간이라고 한다.

 

뷰 공간의 원점은 카메라 렌즈의 정 중아이고, 역시 그로부터 XYZ축을 만들 수 있다.

 

 

 

투영공간

일반 카메라로 사진을 촬영하면 인간의 눈을 통해 보는 것과 동일하게

멀리 있는 물체는 작게 보일 것이다.

 

왜 이렇게 작동하냐면 우리의 시야가 좌우로 100도 정도 상하로 75도 정도로 되어있기 때문이다.

따라서 멀리 바라볼수록 눈에 들어오는 범위가 넓어지는데

 

이 늘어난 범위를 일정한 크기로 망막에 담으려다 보니 멀리있는 물체가 작에 보이게 되는 것이다.

일반 카메라는 사람의 눈을 흉내낸다.

 

물론 이와달리 직교카메라라고 상하좌우로 펴지지 않고 무조건 앞만 바라보는 카메라도 있는데

이걸 사용하면 거리와 상관없이 물체의 크기가 변하지 않는다.

 

결국 카메라로 사진을 찍는 과정을 두 단계로 나눌 수 있는데.

 

첫째는 월드공간에 있는 물체를 카메라 공간으로 이동/회전/확대/축소 시키는 단계이다.

둘째는 이 새로운 공간에 위치된 물체들을 2D 이미지 위에 투영하는 것이다.

 

이러면 첫번째 단계를 뷰공간 / 두번째 단계를 투영공간이라고 구분할 수 있을 것이다.

 

 

직각 투시법이든 원근 투시법이든 뷰공간에는 아무런 영향이 없다.

그 대신 투영공간에서 이 투시법을 적용한다고 생각하면 된다.

 

이렇게 투영까지 마친 결과가 바로 화면에 보여지는 최종적인 결과물이다.

 

 

요약하자면

 

 

3D 그래픽에서 정점위치의 공간을 변환할 때 흔히 사용하는 방법은

정점의 위치벡터에 공간행렬을 곱하는 것이다.

 

물체를 지역공간에서 화면공간까지 옮겨올 때 거치는 공간은

월드공간/뷰공간/투영공간/ 임으로 행렬도 3개를 구해야 한다.

 

참고로 각 공간의 원점과 세 축을 알면 그 공간을 나타내는 행렬을 쉽게 만들 수 있다.(이건 3d 수학책을 참조하자 이 강의에서는 direct3D 에서 제공하는 함수를 이용해서 할 것.)

 

 

전역변수 선언과 정점셰이더 입출력 데이터 선언

이제 우리가 필요한 전역변수들이 무엇인지 알게 되었다.

공간 변환을 할 때 사용할 월드행렬/뷰행렬/투영행렬이 필요하다.

 

따라서 정점 셰이더 코드에 다음의 세 줄을 삽입한다.

float4x4 gWorldMatrix;
float4x4 gViewMaatrix;
float4x4 gProjectionMatrix;

부동소수점 행렬을 선언했는데 누가 이 변수에 값을 전달해줄까?

 

보통 게임에서는 전역변수들의 값을 대입해주는 코드가 존재한다.

우리가 사용한 렌더몽키에서는 변수 시맨틱을 통해 변수 값을 대입해줄것이다.

 

이렇게 행렬도 지정해주고 말이다.

 

 

정점셰이더 함수

이제 모든 준비과정은 끝났다.

드디어 정점셰이더 함수를 작성할 때가 왔는데 먼저 함수 헤더부터 살펴보자.

 

VS_OUTPUT vs_main( VS_INPUT Input )
{
}

이 함수 헤더가 의미하는 바는 다음과 같다.

 

이 함수헤더의 이름은 'vs_main' 이다.

이 함수의 인수는 VS_INPUT 데이터형의 Input 이다.

이 함수의 반환 값은 VS_OUTPUT 이다.

 

이것은 C에서 함수를 정의하는 것과 별반 차이가 없다. HLSL은 C와 비슷한 문법을 가지고 있기 때문이다.

다음 줄을 보자.

VS_OUTPUT Output;

이건 그냥 함수의 끝에서 반환할 구조체를 선언한 것 뿐이다.

 

함수 헤더에서 선언했다시피 데이터형이 VS_OUTPUT인데

VS_OUTPUT의 맴버로는 투영공간으로 변환된 mPosition이 있었다.

 

이제 공간변환을 해볼 차례인데

 

우선 Input.mPosition에 담긴 모델공간 위치를 월드공간으로 변환한다.

공간변환은 어떻게하더라? 아! 정점 위치에 행렬을 곱하는 것이다!

 

그러면 float4형의 위치벡터와 float4x4 행렬을 곱해야한다.

그러므로 내장함수 mul() 을 이용해서 곱해보자.

Output.mPosition = mul(Input.mPosition, gWorldMatrix);

이 코드는 모델 공간에 존재하는 정점위치 (input.mPosition)에 월드행렬을 곱해서

그 결과를 Output.mPosition에 대입한다는 소리이다.

 

이렇게 똑같은 방식으로 뷰공간과 투영공간으로 변환해주면 된다.

Output.mPosition = mul(Input.mPosition, gViewmatrix);
Output.mPosition = mul(Input.mPosition, gProjectionMatrix);

모델공간에 있는 정점의 위치를 투영공간까지 변환했으므로

이 결과를 반환하는 것으로 정점셰이더를 끝내면 된다.

 

 

 

픽셀셰이더

float4 ps_main() : COLOR0
{   
   return( float4( 1.0f, 0.0f, 0.0f, 1.0f ) );
   
}

픽셀 셰이더의 가장 중요한 임무는 픽셀의 색을 반환하는 것이다.

렌더몽키에서 보여주는 빨강색은 어떻게 표현해야하는 것일까?

 

RGB니까 256으로 해야하는게 아닌가?

 

 

색의 표현방법

빨강색을 RGB로 표현하려고 하면 (255,0,0)을 가장 먼저 떠올리는 이유는

RGB의 각 채널을 8비트로 변환하기 때문일것이다.

 

근데 비트수가 변하면 결과값도 달라져서 그거 상관없이 그냥 통일적으로 색을 표현할 방법이 필요하다.

맞다. 백분율을 사용하면 된다.

 

백분율을 사용하면 비트값에 상관없이 언제나 동일하게 나오기때문에 이것이 셰이더에서 색상을 표현할 때 사용하는 방법이다.

 

 

float4 ps_main() : COLOR
{

이 헤더가 의미하는 바는 다음과 같다.

 

이 함수의 이름은 ps_main이다.

이 함수는 매개변수를 받지 않는다.

이 함수의 반환형은 float4 이다.

이 함수의 반환 값을 백버퍼의 색상(color) 값으로 처리할 것.

 

그 다음 함수에서 return4 (1.0f,0.0f,0.0f,1.0f); 이라고 지정해주면 되는 것이다.

 

렌더 몽키에서는 정점셰이더와 픽셀셰이더를 별도로 컴파일 해줘야한다.