2012년 1월 2일 월요일

[포프의 쉐이더 입문강좌] 04. 기초적인 조명쉐이더 Part 2


이전편 보기



기초설정

  1. 여태까지 해왔던 것과 마찬가지로 렌더몽키 안에서 새로운 DirectX 이펙트를 만든 뒤, 정점쉐이더와 픽셀쉐이더의 코드를 삭제합니다.
  2. 이제 쉐이더의 이름을 Lighting으로 바꾸도록 합시다.
  3. 정점의 위치를 변환할 때, 필요한 gWorldMatrix, gViewMatrix, gProjectionMatrix를 추가하고 변수 시맨틱에 연결해주는 것도 잊지 마세요.


람베르트 모델을 이용해서 난 반사광을 계산하려면 어떤 정보가 필요했었죠? 입사광의 벡터와 표면법선 벡터입니다. 법선 정보는 각 정점에 저장되어 있는 것이 보통입니다. (언제나 그렇지는 않습니다. 나중에 법선매핑(normal mapping)을 배울 때 법선을 구하는 다른 방법을 알아봅니다.) 따라서 정점버퍼로부터 이 정보를 가져와야 합니다. 저번 장에서 정점버퍼에서 UV좌표를 불러오기 위해 별도로 해줬던 설정이 있었죠? 렌더몽키의 작업공간 패널에서 Stream Mapping을 더블클릭 한 뒤, NORMAL이란 새로운 필드를 추가합니다. 법선은 3차원 공간에 존재하는 벡터이니 FLOAT3로 선언해주겠습니다. Attribute Name은 크게 신경 쓰지 않으셔도 되지만 Index를 0으로 해주는 것은 잊지 마세요.

그렇다면 입사광의 벡터는 어떻게 구할까요? 사실 이거 별거 아닙니다. 그냥 광원의 위치에서 현재 픽셀 위치까지 직선을 하나 그으면 그게 입사광의 벡터입니다. 따라서 광원의 위치만 알면 입사광의 벡터는 쉽게 구할 수 있습니다. 그렇다면 광원의 위치는 어떻게 정의할까요? 그냥 '월드에서 (500, 500, -500)에 있는 광원' 정도로 하면 되겠지요? 따라서 광원은 전역변수가 됩니다. 렌더몽키의 작업공간에서 Lighting 위에 마우스 오른쪽 버튼을 누른 뒤, Add Variable > Float > Float4를 선택합니다. 새로 생긴 변수의 이름을 gWorldLightPosition이라 바꾼 뒤, 변수 이름을 더블클릭하여 광원의 위치를 (500, 500, -500, 1)으로 설정합니다.

이 모든 설정을 마쳤으면 작업공간이 다음 그림과 같을 것입니다.

그림 4.6. 기초설정을 마친 렌더몽키 프로젝트

정점쉐이더
일단 전체 소스코드부터 보여드린 뒤, 한 줄씩 차근차근 설명해드리겠습니다.

struct VS_INPUT
{
    float4 mPosition : POSITION;
    float3 mNormal : NORMAL;
};


struct VS_OUTPUT
{
   float4 mPosition : POSITION;
   float3 mDiffuse : TEXCOORD1;
};


float4x4 gWorldMatrix;
float4x4 gViewMatrix;
float4x4 gProjectionMatrix;


float4 gWorldLightPosition;


VS_OUTPUT vs_main( VS_INPUT Input )
{
   VS_OUTPUT Output;


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


   float3 lightDir = Output.mPosition.xyz - gWorldLightPosition.xyz;


   Output.mPosition = mul( Output.mPosition, gViewMatrix );
   Output.mPosition = mul( Output.mPosition, gProjectionMatrix );




   lightDir = normalize(lightDir);


   float3 worldNormal = mul( Input.mNormal, (float3x3)gWorldMatrix );
   worldNormal = normalize( worldNormal );


   Output.mDiffuse = dot(-lightDir, worldNormal);


   return Output;
}


정점쉐이더 입력데이터
우선 '제2장: 진짜 쉬운 빨강쉐이더'에서 사용했던 입력데이터의 구조체를 가져와 보도록 하지요.

struct VS_INPUT
{
    float4 mPosition : POSITION;
};


자, 이제 여기에 법선을 더해야겠죠? 정점버퍼에서 법선을 가리키는 시맨틱은 NORMAL입니다. 법선은 3차원 공간에서 방향을 나타내는 벡터이니 float3가 되겠군요.

struct VS_INPUT
{
    float4 mPosition : POSITION;
    float3 mNormal : NORMAL;
};

정점쉐이더 함수
이번 장에서는 정점쉐이더 함수를 먼저 작성한 뒤, 정점쉐이더 출력데이터와 전역변수를 살펴보겠습니다. 아무래도 함수부터 살펴보는 것이 이해가 더 잘 될 겁니다.

우선 정점위치를 변환하는 코드부터 보겠습니다.

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_OUPUT의 일부로 반환할 것입니다. 그러면 보간기를 통해 보간된 값들이 픽셀쉐이더에 전달될 것이니 픽셀쉐이더는 그냥 그 값을 가져다가 사용하면 됩니다.

이와 반대로 픽셀쉐이더에서 이 계산을 한다면 정점쉐이더가 법선정보를 VS_OUTPUT의 일부로 반환할 것이고, 픽셀쉐이더는 보간된 법선을 읽어와 입사광의 벡터와 내적을 구하겠지요.

어느 쪽에서 계산을 해도 차이가 없다면(사실 정점쉐이더에서 구한 결과와 픽셀쉐이더에서 구한 결과 사이에는 약간의 차이가 있습니다. 하지만 거의 눈에 띄지 않을 정도입니다.) 당연히 성능상 유리한 쪽을 택해야겠죠? 어느 쪽에서 계산을 해야 더 빠른지는 각 함수가 호출되는 횟수를 따져보면 알 수 있습니다. (이 외에도 성능을 저해하는 여러 가지 요인이 있으니 그냥 가이드라인으로만 생각하세요.) 삼각형을 하나 그려보시죠. 이 삼각형을 그릴 때, 정점쉐이더가 몇 번 실행될까요? 삼각형을 이루는 정점의 수가 셋이니 3번 실행됩니다. 그렇다면 픽셀쉐이더는 몇 번 실행될까요? 삼각형이 화면에서 차지하는 픽셀 수만큼입니다. 물론 삼각형이 매우 작아서 화면에서 픽셀 하나 크기 밖에 안 된다면 픽셀쉐이더는 한번만 실행됩니다. 하지만, 보통 삼각형이 차지하는 픽셀 수가 3개는 넘겠지요? 따라서 동일한 계산이라면 픽셀쉐이더 보단 정점쉐이더에서 하는 것이 낫습니다. 그럼, 난 반사광의 계산도 정점쉐이더에서 하겠습니다.

팁 - 동일한 계산을 어느 쪽에서도 할 수 있다면 픽셀쉐이더 보단 정점쉐이더에서 하는 것이 성능 상 유리합니다.

그럼 먼저 입사광 벡터를 만들어 보도록 하지요. 입사광의 벡터는 광원의 위치에서 현재 위치까지 선을 쭈욱~ 그으면 된다고 했죠? 이렇게 선을 쭉 긋는 것을 벡터의 뺄셈이라 합니다. 즉, 현재 위치에서 광원의 위치를 빼면 입사광의 벡터를 구할 수 있습니다. 하지만, 한가지 주의해야 할 점이 있습니다. 3D 수학에서 올바른 결과를 얻으려면 모든 변수의 공간이 일치해야 합니다. 앞서 광원의 위치를 이미 월드공간에서 정의했었죠? 그렇다면 정점의 위치는 어느 공간에 있을까요? Input.mPosition은 지역공간에, Output.mPosition은 투영공간에 있네요. 저희가 필요한 것은 월드공간인데 말이지요. 아까 보여드렸던 정점쉐이더의 코드를 다시 한 번 살펴볼까요? 월드행렬을 곱한 다음에 빈 칸을 좀 남겨둔 것이 보이시죠? 월드행렬을 곱한 직후의 Output.mPosition이 바로 월드공간에서의 위치이니 이것에서 광원의 위치를 빼면 되겠네요. 그럼 월드행렬을 곱하는 코드 바로 밑에 다음의 코드를 추가하죠.

   float3 lightDir = Output.mPosition.xyz - gWorldLightPosition.xyz;

이제 이 벡터의 길이를 1로 만듭시다. 벡터의 길이가 1이면 내적만으로도 코사인 값을 구할 수 있다고 했죠? 이렇게 벡터의 길이를 1로 만드는 과정을 정규화(normalize)라고 한다는 것도 말씀드렸던가요? 수학적으로 단위 벡터를 만들려면 각 성분을 벡터의 길이로 나누면 됩니다. 하지만 그 대신 HLSL에서 제공하는 정규화 함수, normalize()를 사용하도록 하지요.

   lightDir = normalize(lightDir);

이제 입사광의 벡터가 준비되었으니 법선을 가져올 차례지요? 정점쉐이더 입력데이터에 있는 법선을 그냥 사용하면 될까요? 이 법선은 어느 공간 안에 있죠? 정점버퍼에서 곧바로 오는 데이터니까 당연히 물체공간이겠죠? 그렇다면 이 법선을 월드공간으로 변환해 줘야만 제대로 난 반사광을 구할 수 있겠네요.

주의 - 3D 연산을 할 때는 모든 변수들이 존재하는 공간이 일치해야 합니다.

   float3 worldNormal = mul( Input.mNormal, (float3x3)gWorldMatrix );

이 위의 코드에서 Input.mNormal이 float3형이니 월드행렬을 그에 맞게 3 X 3 행렬로 바꾼 거 보이시나요? (float3x3)를 앞에 붙이는 방법으로 캐스팅을 했네요. 4 X 4행렬에서 4번째 행(또는 열)은 평행이동(translation) 값이므로 방향벡터에 아무런 영향도 미치지 않습니다. (동일한 방향을 가리키는 화살표 2개를 다른 위치에 놓는다고 해서 그 방위가 바뀌지 않지요? 따라서 방향벡터에서 평행이동 값은 아무 의미도 없습니다.)

이 벡터를 단위벡터로 만드는 것도 잊지 마세요.

   worldNormal = normalize( worldNormal );

이제 입사광의 벡터와 법선이 모두 준비되었으니 내적을 구할 차례입니다. 내적의 공식이 어떻게 되었었죠? 사실 별로 어려운 공식은 아니었는데 굳이 기억하자니 귀찮군요. 그 대신 HLSL자체에서 제공하는 내적함수, dot()을 사용하겠습니다.

   Output.mDiffuse = dot(-lightDir, worldNormal);

위 코드를 보니 내적을 구한 결과를 mDiffuse라는 출력변수에 대입해줬군요. 근데 위에서 lightDir 대신 -lightDir을 쓴 거 보이시죠? 이렇게 한 이유는 두 벡터의 내적을 구할 때, 화살표의 밑동이 서로 만나야 하기 때문입니다. lightDir을 쓰면 입사광 벡터의 머리가 법선의 밑동과 만나므로 잘못된 결과를 발생시킵니다.

또한 내적의 결과는 실수 하나인데 float3인 mDiffuse에 곧바로 대입해준 거 보이시죠? 이렇게 하면 float3의 세 성분이 모두 이 실수 값으로 채워집니다. dot(-lightDir, worldNormal).xxx을 대입해주는 것과 동일하지요.

이제 간단히 결과를 반환해 줍시다.

   return Output;
}

전역변수
이제 왜 쉐이더함수를 먼저 살펴봤는지 아시겠나요? 아무 설명 없이 '빛의 위치를 전역변수로 선언하겠습니다.'라고 말씀드리기가 뭐해서 였습니다.

다음의 전역변수들을 소스 코드 제일 위에 추가해주세요.

float4x4 gWorldMatrix;
float4x4 gViewMatrix;
float4x4 gProjectionMatrix;


float4 gWorldLightPosition;

정점쉐이더 출력데이터
정점쉐이더 함수를 짜보면서 이미 살펴봤듯이 출력데이터는 mPosition과 mDiffuse입니다. 위치야 float4형에 POSITION 시맨틱을 쓰는 걸 이미 알고 있는데, mDiffuse에는 어떤 형과 시맨틱을 써야 할까요? 두 벡터의 내적을 구하면 그 결과는 벡터가 아닌 숫자 하나입니다. (이것을 스케일러(scalar)라고 합니다. 보통은 스칼라라고 많이 하시는데 스케일러가 맞는 발음입니다.) 따라서 float만 사용해도 사실 큰 문제는 아니지만 나중에 이 값을 픽셀의 RGB값으로 출력할 것이니 그냥 float3를 사용하겠습니다. 그렇다면 시맨틱은 어떻게 할까요? DIFFUSELIGHTING이라는 시맨틱이 존재할까요? 불행히도 그렇지 않습니다. (여기서 COLOR0 시맨틱을 사용하지 않은 이유는 정점쉐이더 2.0 규격에서 COLOR 시맨틱을 사용한면 변수의 값이 0~1 사이로 클램프 되기 때문입니다. 따라서 보간기를 거쳐 펙셀쉐이더에서 이 값을 넘겨받으면 좀 오차가 보이더군요.)쉐이더 프로그래밍을 하다 보면 용도에 딱 맞는 시맨틱이 없는 경우가 종종 있는데, 그럴 때는 그냥 TEXCOORD 시맨틱을 사용하는 게 보통입니다. 최소한 8개(TEXCOORD0 ~ TEXCOORD7)의 TEXCOORD가 존재하니까 별로 모자라는 경우가 없거든요. 여기서는 TEXCOORD1을 사용하겠습니다. (TEXCOORD0 대신 TEXCOORD1을 사용한 이유는 다음 장에서 TEXCOORD0을 텍스처의 UV 좌표로 쓰기 위해서입니다.)

다음의 출력데이터를 소스 코드 제일 위에 추가해주세요.

struct VS_OUTPUT
{
   float4 mPosition : POSITION;
   float3 mDiffuse : TEXCOORD1;
};

픽셀쉐이더
막상 픽셀쉐이더를 짜려고 하니까 뭔가 허무한데요? 정점쉐이더가 난 반사광까지도 계산해 줬으니 픽셀쉐이더가 할 일이라곤 그냥 그 값을 가져다가 출력하는 정도겠네요. 그런데 내적은 결국 코사인 함수니까 -1~1의 결과 값을 가지겠죠? 난반사광의 범위는 0~1이니까 -1이하인 값을 0으로 바꾸도록 하죠. 물론 if문을 사용할 수도 있지만 그보다 훨씬 빠른 HLSL함수를 사용하도록 하죠. saturate()라는 함수는 0 이하의 값을 0으로, 1 이상의 값을 1로 바꿔줍니다. 그리고 이 함수는 성능에 아무 영향을 미치지 않는 공짜 함수입니다.

struct PS_INPUT
{
   float3 mDiffuse : TEXCOORD1;
};


float4 ps_main(PS_INPUT Input) : COLOR
{
   float3 diffuse = saturate(Input.mDiffuse);
   return float4(diffuse, 1);
}

위 코드에서 float4(diffuse, 1)이란 형식으로 float4 변수를 만든 것 보이시나요? float4 변수를 만드는 생성자 정도로 생각하시면 되겠네요.

이제 F5를 눌러 정점쉐이더와 픽셀쉐이더를 각각 컴파일 한 뒤, 미리 보기 창을 보시면 부드러운 난 반사광 효과를 볼 수 있으실 겁니다.

그림 4.7 난 반사광 효과



댓글 36개:

  1. 잘 보고 갑뉘다 ~

    궁금한게 혹시 픽셀당 조명연산을 하면 더 퀄리티가 좋와지

    긴 할거 같은데요. 실제로 이런식의 부하가 많은 연산은 게

    임에서 쓰이지 않죠? 폴리곤 수가 적으면 정점 라이팅 계산

    시 폴리곤 경계가 보이는 수도 있는거 같던데 게임에서 이

    런 저 폴리곤은 안쓰일거 같은데 주로 다 정점 라이팅으로

    쓰나여?

    답글삭제
  2. 현재는 거의 대부분 픽셀당 조명계산을 합니다. 법선매핑(normal mapping)은 픽셀에서 할수밖에 없거든요.. (나중에 법선매핑 다룰 때 보실것임 ^_^)

    법선매핑 같은거 안하고 그냥 단순한 계산이면 정점쉐이더에서 하는데 요즘은 거의 다 법선매핑은 기본으로 하는거 같아요.. (Rage만 빼고? ㅋ)

    답글삭제
  3. 아 뉍! 궁금증 풀렸네요 감사합니다. 벌써 법선매핑이 궁금해지네요 따로좀 찾아보고 선행학습을 ㅋㅋ :D

    답글삭제
  4. 불행히도 법선매핑 강좌는 몇달뒤에 올릴지도 몰라요.. -_- 현재 출판사랑 좀 이야기 중인게 있어서...

    (연재는 반드시 다 하는데... 출판사와 합의하는 거 따라 연재 시기가 조금 늦춰질지도 모른다는...)

    답글삭제
  5. 새해 복 많이 받으세요.

    차근 차근 복습하고 있습니다. 하나 궁금한 점이

    render Monkey 의 shader 코드 안에서 printf 같은 기능을

    넣어 variable 을 출력 할 수 있는 방법이 존재 하나요?

    제가 google 에서 검색 해 본 결과 불가능 하다고 읽었습니다.

    안된다면 variable 값을 볼 수 있는 다른 방법이 존재하나요?

    좋은 글 감사합니다. ^^

    답글삭제
  6. 안될껄요? (사실 안해봐서 모름 -_-;)

    전 디버깅할 때 보통 픽셀 쉐이더에서 그냥 값을 반환해서 본다는... (아주 구시대적이죠.. 넹... 그래도 쉐이더가 C/C++ 코딩보다는 간단하니 그정도로도 충분히 디버깅이 가능하더라는...)

    렌더몽키가 아니라 어플에서 돌리는 경우에는 Pix for Windows라는 DX SDK에 딸려나오는 놈을 써서 디버깅도 가능합니다.

    답글삭제
  7. 강좌 고맙습니다.

    열심히 따라 오고 있습니다.
    복습에 복습을 하면서 ^^

    답글삭제
  8. 죄송합니다.

    댓글 주신 아래 글을 이해 못하겠습니다.

    "전 디버깅할 때 보통 픽셀 쉐이더에서 그냥 값을 반환해서 본다는.."

    랜더 몽키에서 작성한 shader 파일을 .fx 저장 뒤 visual studio + directX 상에서 로드 뒤 랜더 할 때 값을 반환 한다란 말 같은데요.. 사실 이것도 써놓고 보니 아닌거 같고 왜냐면 shader 를 directX 로 돌릴 때 리턴값을 받을 수 있을지도 의문이 가서요..

    좀 더 검색해 보고 물어봐야 하는데.. 궁금한점 우선 올립니다.

    감사합니다. ^^

    답글삭제
  9. 예를 들어 float a란 변수의 값을 알고 싶으면.. 그냥 픽셀쉐이더에서. return float4(a.xxx, 1);을 반환해준단 말이었어요. 만약 a 값이 0.5라면 화면에 중간색 회색이 나오겠죠... 만약 a의 값 범위가 0~1 보다 크다면 그에 적절하게 대충 나눠서.. return float4(a.xxx/100.f, 1); 이런식으로...

    비주얼 디버깅이죠 뭐 ^_^

    답글삭제
  10. 1.
    Output.mPosition = mul(Input.mPosition, gWorldMatrix);
    float3 LightDir = Output.mPosition.xyz - gWorldLightPosition.xyz;
    Output.mPosition = mul(Output.mPosition, gViewMatrix);
    Output.mPosition = mul(Output.mPosition, gProjectionMatrix);

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

    Output.mPosition = mul(Output.mPosition, gViewMatrix);
    Output.mPosition = mul(Output.mPosition, gProjectionMatrix);
    float3 LightDir = Output.mPosition.xyz - gWorldLightPosition.xyz;


    위의 코드와 아래 코드가 결과값이 다르게 나오는데, 본문에 설명하신 대로라면 위의 코드( 그럼 월드행렬을 곱하는 코드 바로 밑에 다음의 코드를 추가하죠.)가 맞는거 같은데, 또 포스팅 처음에 완전한 코드를 보면 아래가 맞는건가요?

    암것도 모르는 디자이너라 좀 어렵습니다. 어떤게 맞는건지요?

    답글삭제
  11. 우왓 감사합니다.. 1번이 맞아요! 첨에 나온 포스팅이 잘못된 거에요..가서 본문 고쳐야겠네요 ^^ 꼼꼼히 읽어주셔서 너무너무 감사합니다!

    답글삭제
  12. 제가 오히려 감사드립니다. 좋은 강좌 계속 부탁드립니다. ^-^

    답글삭제
  13. 감사합니다 ^^ 으왕~

    답글삭제
  14. 좋은 강좌 써 주셔서 감사합니다.
    제가 잘 이해를 못하는 부분이 있어 질문 드립니다.

    struct VS_INPUT
    {
    float3 mNormal : NORMAL; --> StreamMapping 에서 NORMAL 을 float3로 추가하는데요.
    };

    struct VS_OUTPUT
    {
    float3 mDiffuse : TEXCOORD1; --> 요건 StreamMapping 에 왜 추가 안하나요?
    };

    답글삭제
    답글
    1. Stream Mapping은 메모리에 있는 정점버퍼와 정접쉐이더의 입력변수 간의 관계를 매핑해주는 거에요.. 메모리에 있는 정점버퍼는 그냥 바이너리 정보에 지나지 않거든요. 그 메모리를 어떻게 해석해야할지를 정해주는게 스트림 매핑이죠.

      정점쉐이더가 리턴하는 정보는 이미 정점버퍼안에 있는 정보와 다르니 스트림 매핑이 필요없죠. 그 대신 정점쉐이더에서 리턴하는 구조체와 픽셀쉐이더가 입력으로 받는 구조체를 같게해서 픽셀쉐이더가 정점쉐이더에서 리턴된 을 어떻게 해석해야하는지 표현해주는 거에요.

      제1장에서 쉐이더 파이프라인 간략히 설명할 때 보여드린 그림을 다시 한번 보시면 도움이 될지도...


      즉,

      정점버퍼 <---- 스트림 매핑 -----> 정점쉐이더 입력데이터
      정점쉐이더 출력데이터 <------- VS_OUTPUT 구조체 -----> 픽셀쉐이더 입력데이터

      이렇게 이해해주시면 됩니다.

      삭제
  15. 쉽게 이해가 잘 되는 강의 감사합니다.
    근데 난반사광을 계산할 때 정점 쉐이더에서 난반사광을 구하는 것에 의문이 있습니다. 쉐이더는 그러면 화면에 표시되지 않을 정점도 난반사광을 계산합니까? 예를 들면 다른 물체에 가려져서 표현되지 않을 물체이거나 물체의 뒷면 그리고 뷰포트(? 용어가 익숙지 않습니다 ㅠㅠ) 밖에 있는 물체들 말이에요.
    만약 이러한 정점들도 계산이 된다면 픽셀 쉐이더보다 더 많은 계산이 필요한 가능성은 전혀 없습니까?(픽셀이 앵간히 많긴하나 봅니다 ㄷㄷ)

    답글삭제
    답글
    1. 픽셀쉐이더는 화면속에 있는 픽셀에만 실행됩니다. 따라서 화면 밖(상하좌우로 밖... 뷰포트 밖이겠군요)에 있는 픽셀에 대해서는 픽셀쉐이더가 실행되지 않습니다.

      질문하신대로 다른 물체에 가려진 경우에는 어떤 물체를 먼저 그리냐에 따라 다릅니다. 예를 들어 뒤에 있는 물체를 먼저 그린 뒤, 앞에 있는 물체를 그리면 픽셀을 두번 그리게 됩니다. 그러나 앞에 있는 물체를 먼저 그린 뒤, 뒤에 있는 물체를 그리면 깊이버퍼라는 놈이 뒤에있는 픽셀을 그냥 무시해줍니다.

      이걸 overdraw(덮쳐그리기?)라고 하는데... 최적화하고 관련이 있죠. overdraw가 적을수록 속도가 빠르겠죠? 그래서 불투명 물체를 그릴때는 화면에 가까이있는것부터 그려줘야합니다. (front-to-back 소팅이라고도 하죠) (물론 깊이버퍼를 꺼버리면 그냥 다 덥쳐그립니다.. 결과가 올바르지 않을뿐... 뒤에 있는게 앞에 있는 물체위를 덥쳐버린다던가... 아..덥친데.. 야해~ -_-)

      뒷면 같은 경우는... 그래픽 카드에서 뒷면추리기(backface culling)을 통해 삼각형 자체를 제거해주는게 일반적이므로 픽셀이 존재하지도 않습니다. (물론 뒷면추리기를 꺼줄수도 있습니다)

      뭐든간에 이런 저런 기법을 통해 덥쳐그리는 일이 적게 만드는게 GPU 최적화의 기본입니다.

      삭제
  16. 스케일러...
    드디어 뜻이 이해가 가는군요.
    요즘은 모르겠지만 20년 전쯤에
    교과서에는 스칼라로 나왔었죠.

    답글삭제
    답글
    1. 저도 예전에 스칼라로 듣고.. 무슨 ... 여자이름 스칼라인줄 알았다죠.. .ㅎㅎ 영어용어 쓰는건 상관이 없는데.. 좀 의미해석은 해줬으면 좋겠어요.

      삭제
  17. 변수 선언 방법에 대해 질문좀 드릴께요.
    월드,뷰,투영 행렬 변수들은 workspace -> add variable을 통해 추가시키고
    variable sementic으로 시멘틱을 연결 해줬는데 gWorldLightPosition 변수는
    workspace에서 만든것 까진 똑같은데 시멘틱 연결 같은건 안해줬어요. 이게 이해가 안가네요.
    시멘틱을 연결해야 하는 기준같은게 있으면 설명좀 부탁드릴께요.

    답글삭제
    답글
    1. 기준은... 없습니다. -_-;;;

      렌더몽키에서 시맨틱 연결이란게.. 렌더몽키안에서 자체적으로 지원하는 정보가 있으면 시맨틱으로 제공을 하는거고 없으면 안하는 겁니다 ㅎㅎㅎ 한마디로 주로 사용하는 정보들을 이미 시맨틱을 제공한다고 보시면 되는 겁니다.

      삭제
  18. saturate 함수가 성능에 영향을 미치지 않는 함수라고 되어 있는데요. 어떻게 구현되어 있길래 그게 가능한지 궁금합니다. 검색을 해봐도 안 나오네요.

    답글삭제
    답글
    1. 예전에 xbox 360 개발자 포럼에서 읽은거라 직접 글을 복사해서 보여드리진 못하구요... gpu자체 레지스터에서 값을 읽어올때 flag하나만 설정하는 것만으로 별도의 연산없이 0~1사이로 saturate() 된 값을 가져올 수 있다고 합니다.

      한마디로 레지스터에서 값을 읽어올때 이런 저런 flag을 설정하는 걸 지원하는데 그중 하나가 0~1사이의 값을 가져오는걸로 알고 있습니다.. (마찬가지로 +값을 -값으로 바꾸는 것도 성능에 영향을 안미친다고 알고 있습니다.)

      삭제
  19. 강좌 잘보고 있습니다. 책에 보면 정점 데이터의 아웃풋 데이터
    float3 mDiffuse : TEXCOORD1; 이 부분에서
    COLOR0 시맨틱을 사용하지 않는 이유는 정점셰이더 2.0규격에서 COLOR시맨틱 사용시 변수의 값이
    0에서 1사이로 클램프되기 때문에 보간기를 거쳐 픽셀 셰이더에서 넘겨받으면 조금 오차가 있다 라고 되어있습니다.
    하지만 결국 saturate 함수에서 클램프를 하므로 똑같은것 아닌가요? 그러면 saturate할 필요도 없고요. 실제로 COLOR시맨틱으로 해봐도 결과는 눈으로 확인하면 거의 같던데. 실제로 어떤 차이가 있을까요?

    답글삭제
    답글
    1. 오래되서 잘 기억이 안나지만... 0을 넘어가 minus가 되는 부분에서 좀 차이가 보였던거 같습니다. -0.1과 0.1을 보간해서 saturate하는것과 -0.1을 0으로 clamp한뒤 0.1과 보간하는 건 값 차이가 있거든요. 특히 사람눈이 어두운 색상의 미세한 변화를 더 잘 감지하니까... 그쪽에서 잘 보였던걸로 기억합니다..(오래되서 기억이 안나요 ㅎㅎ)

      그리고 눈에 보이는 차이가 미묘하더래도 COLOR0을 굳이 잘 사용안하므로 사용하지 말자는 의미에서도 TEXCOORD1을 일부로 쓴겁니다.

      삭제
  20. 덕분에 열심히 공부 잘 하고 있습니다. 혹시, 7장 이후 강좌에 대한 질문은 어디에 올리면 되나요? 책에만있는 내용얘기입니다.

    답글삭제
    답글
    1. 아래 링크에 올리시면 됩니다.

      http://kblog.popekim.com/p/blog-page_1.html

      삭제
  21. 강좌(=책)으로 공부하다 갑자기 질문이 생겨 글 올립니다. Diffuse는 float3형이고, TEXCOORD는 float2형인데 어떻게 서로 다른 데이터형을 시멘틱으로 쓰는 건지요? (정확히는 서로 다른 형(type)에 대해 시멘틱을 쓰면 어떻게 작동되는 건지 궁금합니다.) 또 같은 질문이 될 것 같은데, 정점의 위치정보는 float3로 충분한데 어째서 float4를 할당하는 것인지요?

    답글삭제
    답글
    1. 질문한 내용이 모두 본문에 있네요.
      정독한번 더 하시면 좋을 것 같습니다.

      삭제
  22. 아직 완전히 이해는 못했지만 무작정 따라써가면서 이해하고있는 고3입니다.
    텍스쳐를 입히는 부분 까지는 완벽했으나 조명에서 문제가 생기네요

    float4x4 matViewProjection;
    float4x4 gViewMatrix,gProjectionMatrix,gWorldMatrix;
    float4 gWorldLightPosition;

    struct VS_INPUT
    {
    float4 Position : POSITION0;
    float2 mTexCoord : TEXCOORD0;
    float3 mNormal : NORMAL;


    };

    struct VS_OUTPUT
    {
    float4 Position : POSITION0;
    float2 mTexCoord : TEXCOORD0;
    float3 mDiffuse : TEXCOORD1;
    };

    VS_OUTPUT vs_main( VS_INPUT Input )
    {
    VS_OUTPUT Output;

    Output.Position = mul(Input.Position, gViewMatrix);
    Output.Position = mul(Output.Position, gWorldMatrix);
    Output.Position = mul(Output.Position, gProjectionMatrix);

    Output.mTexCoord = Input.mTexCoord;

    float3 lightDir = Output.Position.xyz-gWorldLightPosition.xyz;

    lightDir = normalize(lightDir);

    float3 worldNormal = mul(Input.mNormal, (float3x3)gWorldMatrix);

    worldNormal = normalize(worldNormal);

    Output.mDiffuse = dot(-lightDir, worldNormal);

    return( Output );

    }



    -----------------------------------------------------

    sampler2D DiffuseSampler;
    struct PS_INPUT
    {
    float2 mTexCoord : TEXCOORD0;
    float3 mDiffuse : TEXCOORD1;
    };
    float4 ps_main(PS_INPUT Input) : COLOR0
    {
    float3 diffuse = saturate(Input.mDiffuse);
    float4 albedo =tex2D(DiffuseSampler,Input.mTexCoord);
    return albedo,float4(diffuse,1);

    }



    프로젝트 세팅은 완전히 맞춰둔 상태입니다. 문제는 과정을 완벽히 카피한듯 한데 화면상에 출력되는건 검은화면뿐이네요..

    답글삭제
    답글
    1. 문제해결에 도움을 주신다면 감사하겠습니다.

      삭제
    2. 코드만 보고 설명 드립니다.

      float3 lightDir = Output.Position.xyz-gWorldLightPosition.xyz;

      이계산을 할 때 output.position이 projection 공간에 있습니다. world공간에 있어야 하는데요. 월드 매트릭스 곱한다음에 곧바로 위 계산을 해주셔야 합니다

      삭제
  23. 안녕하세요? 저도 이부분을 따라해보다가 계속 에러가 발생해서 질문드립니다
    아래와 같은 에러가 나는데요..어디서 잘못된건지 알수가 없네요
    도움 주시면 감사하겠습니다

    Compiling vertex shader API(D3D) /../Lighting/Pass 0/Vertex Shader/
    COMPILE ERROR: API(D3D) /../Lighting/Pass 0/Vertex Shader/ C:\Program Files (x86)\AMD\RenderMonkey 1.82\memory(10,10): error X3000: syntax error: unexpected end of file
    RENDERING ERROR(s):
    Vertex shader 'Vertex Shader' failed to compile in pass 'Pass 0'. See Output window for details

    답글삭제
    답글
    1. 코드를 안보여주시면 답을 다를수가 없는데.... 괄호 {} 를 몇개 놓치신건 아닌지 궁금합니다

      삭제
  24. 포프님 안녕하세요! 청강대에서 선생님이 올려주신 강좌 보면서 열심히 공부중인 학생입니다!
    HLSL에서도 주석기능을 지원하긴 하는거 같은데 한글 주석은 길이 제한이 있나요?
    자꾸 오류가 떠서 뭐가 문제인가 한참 찾다가 한글로 적어둔 주석을 지워봤는데 코드가 정상적으로 작동하더라구요

    한글 주석과 영문 주석에 특별한 차이가 있을까요?

    답글삭제
  25. 안녕하세요. 책으로 공부하다가, 검은 화면만 출력되어 포프님 블로그까지 들어왔습니다. 혹시나 하여, 포프님 코드를 긁어서 붙여넣었는데도 검은 화면이 출력됩니다. 프로젝트 세팅은 완벽하게 맞춰놨는데(gWorldMatrix, gViewMatrix,gProjectionMatrix의 시맨틱 지정과 gWorldLightPosition의 위치와, Stream Mapping의 Normal과 Float3지정)왜 그런지 알 수가 없네요 ㅠ 무슨 문제가 있을까요?ㅠㅠ

    답글삭제