2012년 1월 30일 월요일

[포프의 쉐이더 입문강좌] 06. 만화같은 명암을 입히는 툰쉐이더

이전편 보기

샘플파일 받기

제6장 만화 같은 명암을 입히는 툰쉐이더


이 장에서 새로 배우는 HLSL
  • ceil() - 무조건 올림 함수

이 장에서 새로 사용하는 수학
  • 행렬합치기 - 여러 개의 행렬을 미리 곱해놓은 뒤, 그 결과를 정점변환에 사용해도 결과는 동일함. 단, 속도는 더 빠름.
  • 역행렬 - 반대 방향으로 공간변환을 할 때 유용.


배경
얼마 전에 저희 회사의 아트 디렉터가 했던 말이 있습니다. 프로그래머는 언제나 사실적인 3D 그래픽을 추구하지만, 게이머들의 보통 미적 스타일을 최대로 살린 비사실적인 그래픽에 열광한다고요. 생각해보니  맞는 말이더군요. 프로그래머들은 언제나 수학적으로 옳은 것을 추구하려고 하지만 언제나 미적 스타일을 갖춘 게임들이 흥행을 하니까요. 스트리트 파이터 4, 팀포트리스 2, 보더랜드 등이 그 좋은 예겠죠?

그 동안 현대 3D 그래픽의 주 초점도 사실적인 그래픽을 재현해 내는 것이었습니다. 하지만 그 와중에도 미적인 효과를 살리기 위한 비사실적 렌더링 기법들도 간간이 등장했는데요 여기서 살펴볼 툰쉐이딩(toon shading, 셀 쉐이딩(cell shading)이라고도 합니다) 도 그 중 하나입니다. 툰(toon)이라 하면 만화(cartoon)를 뜻합니다. 만화를 보면 명암처리를 부드럽게 하는 대신에 칼같이 딱딱 끊어서 2~3 단계로 하죠? 뭐, 만화를 안보신 분들은 없을 듯 하니 다 아시겠네요. ^^ 여기서 구현할 쉐이더가 바로 그런 일을 할 겁니다. 일단 결과를 미리 사진으로 보여드리면 대충 감이 오시겠네요.

그림. 6.1. 이장에서 만들어 볼 툰쉐이더

위의 사진을 잘 관찰해 봅시다. 여태까지 사용했던 평범한(?) 난반사광 쉐이더와 뭐가 다르죠? 난반사광이 부드럽게 어두워지는 대신 단계적으로 팍팍 줄어든다는 거죠? 마치 계단을 걸어 내려가는 것처럼요. 그렇다면 이것을 그래프로 그려보면 어떨까요? 일반적인 난반사광의 그래프와 비교해서 보면 좀 더 이해가 쉽겠네요.

그림 6.2  일반 난반사광 그래프와 툰쉐이딩 그래프


위 그래프를 보니 감이 팍팍 오지 않나요? 아니라고요? 으음... 그럼 위 그래프를 표로 간단하게 정리해 보겠습니다..


난반사광의 양 툰쉐이더 값
0 0
0 ~ 0.2 0.2
0.2 ~ 0.4 0.4
0.4 ~ 0.6 0.6
0.6 ~ 0.8 0.8
0.8 ~ 1 1
표 6.1 난반사광의 양과 툰쉐이더 값의 비교

이렇게 비교를 하니 정말 쉽네요. 난반사광의 값을 가져다가 0.2단위로 무조건 올림을 하면 툰쉐이더 값이 나오는군요? 그럼 이 정도만 알면 툰쉐이더를 만드는 건 식은 죽 먹기일 듯 합니다. 곧바로 렌더몽키로 가 볼까요?

기초설정
렌더몽키를 실행한 뒤, DirectX 이펙트를 추가합니다. 새로 생긴 이펙트의 이름을 Default_DirectX_Effect에서 ToonShader로 바꿉니다. matViewProjection이란 행렬이 정의되어 있는 것도 보이시죠? 삭제해 주세요.

그림 6.1에서 주전자 모델을 보여드렸었죠? 주전자는 3D 그래픽 논문에서 즐겨 사용하는 모델 중에 하나입니다. 이리저리 다양한 굴곡이 많아서 쉐이더의 결과를 딱 한눈에 살펴보기 좋다나요? 저희도 주전자 모델을 사용하겠습니다. 렌더몽키의 작업공간 패널에서 Model을 찾으세요. 이 위에 마우스 오른쪽 버튼을 누른 뒤, Change Model > Teapot.3ds를 선택합니다.

툰쉐이딩을 하려면 일단 난반사광을 계산해야겠죠? 그래야 그 결과를 0.2 단위로 올림할 수 있으니까요. 그렇다면 '제4장: 기초적인 조명쉐이더'에서 그랬던 것처럼 빛의 위치와 정점의 법선정보가 필요하겠군요. 우선 빛의 위치를 변수로 선언하겠습니다. ToonShader에 오른쪽 마우스 버튼을 누른 뒤, Add Variable > Float > Float4를 선택하고, 변수의 이름을 gWorldLightPosition으로 바꿉니다. 이 변수의 값은 예전과 마찬가지로 (500, 500, -500, 1)으로 맞춰주세요. 다음은 정점에서 법선정보를 읽어올 차례입니다. Stream Mapping을 더블클릭해서 NORMAL 필드를 추가하면 되겠죠? 데이터형은  FLOAT3, Index는 0으로 해주는 것도 잊지 마세요.

그림 6.1을 다시 한번 봐 보죠. 주전자가 녹색이죠? 주전자의 색을 지정해주는 방법은 여러 가지가 있지만 여기서는 전역변수 하나로 전체 표면의 색을 지정해 주겠습니다. (만약 한 표면 위에서 여러 가지 색상을 사용하시고 싶으시다면 3DS MAX에서 메쉬를 만드실 때, 정점색상(vertex color)를 칠하셔도 됩니다. 그리고 정점쉐이더 입력데이터에서 COLOR0이나 COLOR1 시맨틱을 사용하시면 이 정점정보를 읽어올 수 있습니다.) ToonShader에 마우스 오른쪽 버튼을 누른 뒤, Add Variable > Float > Float3를 선택합니다. 새로 변수가 생기면 이름을 gSurfaceColor로 바꿉니다. 이제 이 변수 위에 마우스를 더블클릭하여 값을 (0, 1, 0)으로 변경합니다. 쉐이더에서 색상을 0~1 사이의 백분율 값으로 표현한다는 거 잊지 않으셨죠?

이제 행렬들을 좀 추가해 주겠습니다. 여태까지 다뤘던 쉐이더 기법에서는 기초설정을 할 때마다 월드행렬, 뷰행렬, 투영행렬을 따로 정의해 줬었죠? 여기서는 조금 다른 방법을 사용하도록 하죠. 3D 그래픽에서 공간변환을 할 때, 행렬을 사용하는 이유 중 하나가 여러 행렬들을 미리 합쳐놓으면(concatenation) 불필요한 연산을 줄일 수 있기 때문입니다. 예를 들면, 정점의 위치를 공간변환 할 때 월드행렬, 뷰행렬, 투영행렬을 차례대로 곱해줘야 하죠? 이러지 말고 미리 월드행렬, 뷰행렬, 투영행렬을 곱해서 새로운 행렬을 하나 구해 놓은 뒤, 그 행렬을 정점에 곱해도 결과는 동일합니다. 그러나 성능상으로 보면 행렬을 3번 곱하는 것보다 1번 곱하는 게 당연히 빠를 테니 행렬을 미리 합치는 방법이 더 낫지요.

여기서도 미리 행렬을 합치겠습니다. 그럼 그 결과 행렬을 건네 받을 전역변수를 하나 추해야겠죠? ToonShader에 오른쪽 마우스 버튼을 눌러 Add Variable > Matrix > Float(4x4)를 선택한 뒤, 변수명을 gWorldViewProjectionMatrix로 바꿉니다. 이제 이 변수 위에 마우스 오른쪽 버튼을 눌러 Variable Semantic > WorldViewProjection을 선택합니다.

자, 그럼 이렇게 행렬을 한 번만 곱하는 건 좋은데 난반사광을 계산하려면 월드행렬이 필요했던 것 같은데요? 빛의 위치가 월드공간에 정의되어 있으니까 빛의 방향벡터를 만들려면 월드공간에서의 정점위치가 필요했었네요. 그럼 당연히 월드행렬을 곱해야겠죠. 그리고 난반사광을 구하려면 역시 월드공간에서의 정점법선도 필요했었으니 역시 월드행렬이 필요하군요. 그렇다면 월드행렬을 전역변수로 전달해줘서 이렇게 행렬곱을 두 번 더 해줘야 할까요? 뭐 그러셔도 상관 없습니다. 틀린 방법은 아니거든요. 하지만 조금만 생각을 더 해보면 행렬곱 1번만으로 똑같은 일을 할 수 있습니다.

정점의 위치와 법선을 월드공간으로 변환하는 이유는 빛의 위치가 월드공간에 정의되어 있어서였습니다. 모든 변수가 동일한 공간에 있어야만 올바른 결과를 구할 수 있으니까요. 그럼 정점의 위치와 법선벡터를 월드공간으로 변환하는 대신에 빛의 위치를 지역공간으로 변환해버리면 어떨까요? 그러면 정점의 위치와 법선에 손을 대지 않아도 모든 매개변수들이 동일한 공간에 있겠죠? 이 방법은 행렬을 1번만 곱하니 아무래도 조금 더 빠르겠네요.

그렇다면 월드공간을 물체공간으로 어떻게 변환할까요? 월드행렬의 역행렬(inverse matrix)을 곱하면 됩니다. 그럼 렌더몽키에 월드행렬의 역행렬도 추가해 보도록 하죠. ToonShader에 마우스 오른쪽 버튼을 누른 뒤, Add Variable > Matrix > Float(4x4)를 선택합니다. 이제 이 변수 이름을 gInvWorldMatrix로 바꿔 주세요. 마지막으로 변수 위에 마우스 오른쪽 버튼을 눌러 Variable Semantic > WorldInverse를 누르면 모든 설정이 마무리 되었습니다.


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


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

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

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

float4x4 gWorldViewProjectionMatrix;
float4x4 gInvWorldMatrix;

float4 gWorldLightPosition;

VS_OUTPUT vs_main( VS_INPUT Input )
{
   VS_OUTPUT Output;

   Output.mPosition = mul( Input.mPosition, gWorldViewProjectionMatrix );
 
   float3 objectLightPosition = mul( gWorldLightPosition, gInvWorldMatrix);
   float3 lightDir = normalize(Input.mPosition.xyz - objectLightPosition);
 
   Output.mDiffuse = dot(-lightDir, normalize(Input.mNormal));
 
   return( Output );
 
}



정점쉐이더 입출력데이터 및 전역변수
조명(난반사광)을 계산하려면 법선이 필요하죠? 따라서 정점쉐이더 입력데이터로 위치와 법선이 필요합니다. (정점마다 색을 지정해주셨다면 float3 mColor : COLOR0; 도 추가하셔야 합니다. Stream Mapping에서도 COLOR0을 더해주는 거 잊지 마세요.)

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

정점출력데이터도 별로 어렵지 않습니다. 난반사광을 계산한 뒤, 픽셀쉐이더에 전달해 주는 게 전부입니다. (마찬가지로 정점마다 색을 지정해주셨다면  float3 mColor: COLOR0;도 추가하셔야 합니다.)  이게 잘 이해가 안 되시는 분들은 '제4장: 기초적인 조명쉐이더'를 다시 한 번 읽어 주세요.

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

이제 전역변수로는 위에서 설명 드렸던 행렬 2개와 광원의 위치를 선언해야겠네요.

float4x4 gWorldViewProjectionMatrix;
float4x4 gInvWorldMatrix;

float4 gWorldLightPosition;

이러면 앞서 렌더몽키 프로젝트에 더했던 변수들을 다 처리한 거 같죠? 이제 정점쉐이더 함수를 보겠습니다.

정점쉐이더 함수
우선 정점쉐이더의 가장 중요한 임무를 수행하겠습니다. 정점의 위치를 투영공간으로 가져옵니다. 월드행렬, 뷰행렬, 투영행렬을 하나로 미리 합쳐버렸으니 코드 한 줄로 이런 일을 할 수 있겠네요.

VS_OUTPUT vs_main( VS_INPUT Input )
{
   VS_OUTPUT Output;

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


이제 난반사광의 양을 계산할 차례입니다. 앞서 말씀 드렸듯이 빛의 위치를 지역공간으로 변환한 뒤, 모든 계산을 이 공간에서 해보겠습니다. 우선 빛의 위치를 지역공간으로 변환합니다.

   float3 objectLightPosition = mul( gWorldLightPosition, gInvWorldMatrix);

이제 광원의 위치에서 현재 위치(이미 지역공간에 있습니다)를 가리키는 방향벡터를 만듭니다. 이 방향벡터의 길이를 1로 만드는 것도 잊지 말아야겠죠?

   float3 lightDir = normalize(Input.mPosition.xyz - objectLightPosition);

이제 그 결과와 정점의 법선(역시 지역공간에 존재합니다) 간의 내적을 구하면 난반사광의 양을 구할 수 있습니다.

   Output.mDiffuse = dot(-lightDir, normalize(Input.mNormal));

위에서 법선의 길이를 1로 만들기 위해 normalize()함수를 호출한 거 보이시죠? 보통 정점버퍼로부터 곧바로 가져온 법선은 이미 정규화가 되어있는 게 보통이나 혹시나 해서 normalize()를 한 번 더 호출해 봤답니다.

이제 가볍게 Output을 반환합니다.

   return( Output );
 
}

정점쉐이더 함수는 별로 어려운 게 없었습니다. '제4장: 기초적인 조명쉐이더'에서 다 배웠던 내용이니까요. 그냥 다른 공간을 사용했다는 게 좀 다른 내용이지만 그리 어렵지 않게 이해하시리라 믿습니다. 이제 픽셀쉐이더를 살펴보겠습니다.

픽셀쉐이더
정점쉐이더에서와 마찬가지로 전체 소스코드부터 보여드립니다.

float3 gSurfaceColor;

struct PS_INPUT
{
   float3 mDiffuse : TEXCOORD1;
};

float4 ps_main(PS_INPUT Input) : COLOR
{
   float3 diffuse = saturate(Input.mDiffuse);
 
   diffuse = ceil(diffuse * 5) / 5.0f;
 
   return float4( gSurfaceColor * diffuse.xyz, 1);
 
}

우선 전역변수와 픽셀쉐이더 입력데이터를 정의하죠. 표면의 색상을 전역변수로 선언하고 정점쉐이더에서 계산을 마친 난반사광의 양을 입력데이터로 받겠습니다.

float3 gSurfaceColor;

struct PS_INPUT
{
   float3 mDiffuse : TEXCOORD1;
};

이제 픽셀쉐이더 함수를 봅시다. 우선 mDiffuse에서 저희에게 별 의미가 없는 0 이하의 값을 잘라 냅니다.

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

이제 이 값을 0.2단위로 딱딱 잘라야겠네요. 0.2단위로 무조건 올림을 하면 된다고 했었죠? HLSL에서 무조건 올림을 하는 함수는 ceil()입니다. (영어로 ceiling이 '천장'이라는 뜻이니까, ceil을  '천장으로 올리다' 정도로 생각하시면 이해에 도움이 되실 겁니다. 이 반대로 무조건 내림을 하는 함수로 floor()입니다. 이것은 '바닥으로 내리다' 정도로 이해하세요.) 근데 ceil() 함수는 언제나 바로 위의 정수로만 올림을 한다는군요. 저희는 0.2단위로 올림을 해야 하는데 어쩌죠? 다음과 같이 간단히 곱셈과 나눗셈을 하면 됩니다.

   diffuse = ceil(diffuse * 5) / 5.0f;

위 공식(?)을 자세히 살펴보죠. diffuse가 0~1 사이의 값이니 여기에 5를 곱하면 범위가 0~5가 될 것입니다. 여기에 ceil()을 적용하면 그 결과값이 0, 1, 2, 3, 4, 5중에 하나가 되겠죠. 이제 이 값을 5로 나누면 최종 결과값이 0, 0.2, 0.4, 0.6, 0.8, 1 중에 하나가 될 겁니다. 이게 바로 저희가 원하는 값 맞죠? 그림 6.2와 표 6.1를 다시 봐도 이 값이 맞네요.

그럼 이제 표면의 색을 곱하기만 하면 끝입니다. (빛의 색상은 흰색(1, 1, 1)이라고 가정했습니다. 어떤 수에 1을 곱해도 결과는 바뀌지 않는 거 아시죠?)

   return float4( gSurfaceColor * diffuse.xyz, 1);
}

이제 F5를 눌러 정점쉐이더와 픽셀쉐이더를 각각 컴파일 한 뒤, 미리 보기 창을 보시면 그림 6.1와 똑같은 주전자가 보이죠? 아, 배경색이 다르다고요? 미리 보기 창 안에서 마우스 오른쪽 버튼을 누른 뒤 Clear Color를 선택하시면 배경색을 바꿀 수 있습니다.

선택사항: DirectX 프레임워크
이제 C++로 작성한 DirectX 프레임워크에서 쉐이더를 사용하시고자 하는 분들을 위한 선택적인 절입니다.

우선 '제3장: 텍스처매핑'에서 만들었던 프레임워크의 사본을 만들어 새로운 폴더에 저장합니다. 그 다음은 렌더몽키에서 사용했던 쉐이더와 3D 모델을 DirectX 프레임워크에서 사용할 수 있도록 파일로 저장할 차례입니다.


  1. Workspace 패널에서 ToonShader를 찾아 오른쪽 마우스 버튼을 누릅니다.
  2. 팝업메뉴에서 Export > FX Exporter를 선택합니다.
  3. 위에서 새로 만든 폴더를 찾아 그 안에 ToonShader.fx란 이름으로 파일을 저장합니다.
  4. 이제 Worspace 패널에서 Model을 찾아 오른쪽 마우스 버튼을 누릅니다.
  5. 팝업메뉴에서 Save > Geometry Saver를 선택합니다.
  6. 위에서 새로 만든 폴더를 찾아 그 안에 Teapot.x란 이름으로 파일을 저장합니다.


이제 비주얼 C++ 에서 프레임워크의 솔루션 파일을 엽니다.

우선 예전에 있던 전역변수들부터 살펴보겠습니다. gpTextureMappingShader 변수가 있는데 이 변수가 언급되어 있는 곳을 모두 찾아 gpToonShader로 바꿉니다. gpSphere변수도 똑같은 방법으로 모두 gpTeapot으로 바꿔 줍니다. 텍스처 변수, gpEarthDM도 있네요. 여기서는 텍스처를 전혀 사용하지 않으니 gpEarthDM변수를 사용하는 코드를 모두 찾아 삭제해주세요.

이제 새로 추가해야 할 전역변수들을 알아볼까요? 새로 추가해야 할 전역변수는 빛의 위치와 표면의 색상밖에 없는 것 같군요. 다음의 코드를 추가합니다.

// 광원의 위치
D3DXVECTOR4 gWorldLightPosition = D3DXVECTOR4(500.0f, 500.0f, -500.0f, 1.0f);

// 표면의 색상
D3DXVECTOR4 gSurfaceColor =       D3DXVECTOR4(0, 1, 0, 1);

위 코드에서 전역변수를 선언할 때 렌더몽키에서 사용했었던 값도 그대로 대입해줬습니다.

이제 LoadAssets() 함수로 가서 로딩해올 쉐이더와 모델의 이름을 각각 Toonshader.fx와 Teapot.x로 바꿔줍니다.

bool LoadAssets()
{
    // 텍스처 로딩

    // 쉐이더 로딩
    gpToonShader = LoadShader("ToonShader.fx");
    if ( !gpToonShader )
    {
        return false;
    }

    // 모델 로딩
    gpTeapot = LoadModel("Teapot.x");
    if ( !gpTeapot )
    {
        return false;
    }

    return true;
}

다음은 실제로 장면을 그리는 RenderScene() 함수입니다. 행렬 2개를 새로 전달해줘야 했었죠? 월드/뷰/투영행렬을 합친 행렬과 월드행렬의 역행렬이었습니다. 우선 월드행렬의 역행렬을 구해봅시다. 월드행렬을 구했던 코드 아래에 다음의 라인을 추가합니다.

    // 월드행렬의 역행렬을 구한다.
    D3DXMATRIXA16 matInvWorld;
    D3DXMatrixTranspose(&matInvWorld, &matWorld);

위 코드에서 사용한 D3DXMatrixTranspose() 함수는 전치행렬(transpose matrix)을 구합니다. 여기서 역행렬 대신에 전치행렬을 구한 이유는 월드행렬이 직교행렬이기 때문이죠. 직교행렬의 전치행렬은 역행렬과 같습니다.(이에 대한 증명 및 자세한 설명은 이미 시중에 나와있는 훌륭한 수학책을 참고하시기 바랍니다. 참고로 순수하게 역행렬을 구하시려 한다면 D3DXMatrixInverse() 함수를 쓰시면 됩니다.)

이제 월드/뷰/투영행렬을 서로 곱할 차례입니다. D3DXMatrixMultiply() 함수를 사용하겠습니다.

    // 월드/뷰/투영행렬을 미리 곱한다.
    D3DXMATRIXA16 matWorldView;
    D3DXMATRIXA16 matWorldViewProjection;
    D3DXMatrixMultiply(&matWorldView, &matWorld, &matView);
    D3DXMatrixMultiply(&matWorldViewProjection, &matWorldView, &matProjection);

월드행렬 X 뷰행렬 X 투영행렬 순으로 곱해준 거 보이시죠?

이제 위에서 만들었던 두 행렬을 쉐이더에 전달해 주겠습니다. 예전에 사용했던 SetMatrix() 함수 호출들을 다 지우시고 아래의 코드를 대신 삽입해주세요.


    // 쉐이더 전역변수들을 설정
    gpToonShader->SetMatrix("gWorldViewProjectionMatrix",
        &matWorldViewProjection);
    gpToonShader->SetMatrix("gInvWorldMatrix", &matInvWorld);

마지막으로 광원의 위치와 표면의 색상을 전달해주는 것도 잊지 마셔야겠죠?

    gpToonShader->SetVector("gWorldLightPosition", &gWorldLightPosition);
    gpToonShader->SetVector("gSurfaceColor", &gSurfaceColor);

이제 코드를 컴파일 한 뒤, 프로그램을 실행하시면 빙글빙글 도는 주전자를 보실 수 있을 겁니다. 아무래도 회전을 하니까 손잡이나 주둥이에서 툰쉐이더 효과가 더 잘 나타나죠?

정리
다음은 이 장에서 배운 내용을 짧게 요약해 놓은 것입니다.

  • 툰쉐이더는 비실사 렌더링 기법 중에 하나이다.
  • 툰쉐이딩은 난반사광을 단계적으로 감소시키는 것에 지나지 않는다.
  • 행렬들을 미리 곱해놓으면 공간변환을 더 빨리 할 수 있다.




다음편 보기



댓글 27개:

  1. 툰 쉐이딩 기법 잘 봤습니다.

    요새 나루토라던지 드래곤볼 게임들 보면 정말 이쁘게 툰쉐이

    딩이 들어갔더라구요... 다 여기서 업그레이드 된거겠져? ㅋㅋ

    답글삭제
    답글
    1. 그쵸. .이건 정말 기초중의 기초 기법이에요.. 게임에 넣을법한 툰쉐이딩 기법은 제가 직접 만들어 본적조차 없어서... ^^ 제가 쓰면 욕먹어요~

      삭제
  2. 기다림... 어떻게 기다리지... 힘들겠다.
    빨리 보고 싶은데... 출판일이 4~5월이라고 하셨죠?
    책이든 온라인이든 기다려야 하군요.
    기다리는 동안 볼만한 다른 책이나 사이트(가능하시면 한글로 된 것)를 추천해 주세요.
    DirectX 이나 Shader 관련되는 것도 좋고 3D programming에 필요한 기본 지식에 관련 것도 좋아요

    그동안 감사히 잘 봤습니다.

    답글삭제
    답글
    1. 제가 한글 책을 안봐서 모르는 관계로.. 다른 분들께 물어다 드리지요. 강좌 연기되는건 죄송합니다. 그냥 책도 나오는게 가장 나은 방법이라 생각했어요...

      삭제
    2. 해골 책이래요...

      http://twitter.com/#!/lostland/status/163865330307969024

      삭제
    3. "(용책) bit.ly/zU31HS (해골책) bit.ly/yhx03h (셰이더) bit.ly/xVAGgF (3차원 그래픽스) bit.ly/uDLlVN 이정도쯤...?!"

      이라고 yoonheecode님이 남겨주셨어요.
      http://twitter.com/#!/yoonheecode/status/163919637074026497

      삭제
    4. 좋은 책을 추천해 주셔서 정말 감사합니다.
      추천하신 책중 해골책과 셰이더책은 겉핧기식으로 읽어봤습니다.
      다시 읽어봐야 겠습니다.
      용책과 3차원 그래픽스 관련 책도 구해서 같이 읽으며 4~5월을 기다려야 겠습니다.
      그래도 여기에 자주 들릴테니 책이 나오면 바로 소식 전해 주세요.

      다시한번 좋은 강좌에 감사 드립니다.
      (셰이더에 대한 답답했던 부분들이 뻥뻥 뚫리는 기분을 저에게 주셨습니다.)

      삭제
    5. 답답했던 부분들이 뚤렸다면 제 할일은 다했군요 ^_^ 그게 이 책 쓴 유일한 이유였으니까요.. 공부 열심히 하세요~

      삭제
  3. 저도 그렇게 텍스처 쓰는 기법을 몇번 봤는데.. 왜 그러는진 아직 모르겠어요. 텍스처 unit에 보틀넥만 안걸리면 그게 더 빠를수도 있죠. (요즘 대부분의 쉐이더는 ALU(계산유닛) 부하 걸리는게 보통이니.. 그게 더 나으려나....)

    답글삭제
  4. 간소화(?) 최적화(?) 한 부분에서 한참 생각 했습니다.
    월드공간 , 지역 공간 ...다시 앞장을 정독해야겠습니다.

    그래도 주전자에 이쁘게 단계별로 나왔습니다.

    P.S 월요일이 기다려졌습니다.4~5월까지 어찌 기다릴까요?
    그 동안 열심히 앞 강좌 정독을 하겠습니다.

    강좌 고맙습니다. ^^

    답글삭제
    답글
    1. 솔직히 다음강좌가 법선매핑이라... 거기까진 올렸으면 했는데 아무리 페이지를 따져봐도 그러면 절반을 훨 넘더라구요...... 봄을 만끽하면서 기다려주시길!

      삭제
  5. 그동안 강좌 올려주셔서 정말 잘봤습니다..감사하구요

    궁금한게 하나 있는데 시맨틱중에서 WorldInverse 와 WorldInverseTranspose 두 시맨틱의

    차이가 있을까요?

    답글삭제
    답글
    1. 렌더몽키에서 말씀하시는거죠? transpose는 보통 행우선/열우선 행렬을 사용하느냐 따라 다른거에요. transpose안된거가 행우선 행렬이라면 transpose된거는 열우선 행렬.. 아님 그 반대...

      뭐 행렬을 어떻게 메모리에 넣는냐 차이인데.. -_- 엔진따라 다르고... 프로그램따라 다르니까....

      벡터와 행렬을 곱할 때 mul(position, matrix);냐 mul(matrix, position)으로 하느냐 차이만 생겨요.. 뭐 프로그래머 아니시라면 굳이 자세히 이해해야할 필요는 없다고 생각 -_-

      삭제
    2. 아 감사합니다. 랜더몽키랑 FX컴포져랑 같이 공부하고 있거든요.
      답변 감사합니다. 프로그래머는 아니지만 궁금했던 부분이 해소되서 다행이네요^^

      포프님 덕에 공부많이 하고 갑니다.

      삭제
  6. 포프님 책 보면서 이것저것 실험 중인 학생인데요
    궁금한게 있어서 질문좀..
    pass를 하나짜리로 하면 잘 되는데
    2개짜리로 하면 두번째 패스가 실행이안되는데 왜 그런거지요 ㅠ,ㅜ

    답글삭제
    답글
    1. 책 어딘가에 써놨는데.. pass니 technique이니 하는건 그냥 정점쉐이더/픽셀쉐이더 쌍을 여러개를 분류해놓는 도구에 지나지 않습니다.

      현재 렌더몽키에서 패스를 2개하면 첫번째 두번째 자동으로 실행될거에요 아마. DX에서 안되시는 거라면.... 처음가오자에서 보여드렸듯이..

      gpColorShader->Begin(&numPasses, NULL);
      {
      for (UINT i = 0; i < numPasses; ++i )
      {
      gpColorShader->BeginPass(i);
      {
      // 구체를 그린다.
      gpSphere->DrawSubset(0);
      }
      gpColorShader->EndPass();
      }
      }
      gpColorShader->End();

      이렇게 for loop로 두번다 그려주셔야 합니다. 사실 이렇게하면 쉐이더를 바꿔주면서 메쉬를 두번그려주는것밖에 안되죠. 이래도 안된다면 for loop를 0부터 numPasses까지 돌리지 마시고 1부터 numpasses까지 돌려보세요.

      1부터 했을때 결과가 제대로라면.. 아마 깊이 버퍼 에서 rejection되면서 안그려지는 걸겁니다... (근데 default 깊이 테스트가 Less or Equal이라서 그럴일은 없지 않을까 싶을텐데요. 한번 깊이 테스트 모드를 LEQUAL로 직접 설정해줘보세요..)

      이래도 안되면 다시 답글 달아주세요.

      삭제
  7. 포프님 책을보면서 쭉 따라하다가 마지막에 외곽선 부분을 해보았습니다.
    일단 프레임워크에 적용하는데 까지는 다했습니다.
    렌더몽키 작성은 어떤씩으로 했냐하면 Pass0을 툰으로 하고 Pass1을 외곽선으로 했습니다.
    렌더타겟은 책에 나온데로 그대로 했고요.

    그래서 일단 외곽선이 나오니 좋아했습니다.
    근데 SMD모델을 불러왔더니 정말 외곽선만 나오고 모델텍스처가 전혀 나오질 않았습니다.
    SMD만 따로 부르면 모델이 멀쩡히 잘나오구요..(그래도 나름 이것저것 해본다고 했는데 멘붕..)
    이부분은 어떤씩으로 해야하나요? 얼마전부터 뻘짓하다가 이렇게 글을 남기게 되었습니다..

    답글삭제
    답글
    1. 죄송합니다 댓글이 좀 늦었습니다.

      책속에 있는 외곽선 기법은 그냥 수학적으로 색이 바뀌는 부분을 다른색으로 보여주는 저옫입니다. 아마 툰 셰이딩에서 외곽선 그리기 기법을 생각하시는듯 한데 그건 전혀 다른 기법입니다.

      예전에 어떤 분이 gamedevforever에 적어놓으신 글이 있어서 아래 그 링크를 걸어둡니다. 아마 이걸 원하시는거라고 생각합니다. 혹시 아니라면 다시 댓글 달아주세요.

      http://www.gamedevforever.com/18

      p.s 근데 SMD모델이 뭐죠? -_-

      삭제
  8. 답글 감사합니다 ! 그런데 이미 해결을 해버렸네요.. ㅠㅠ
    픽셀 쪽에
    float3 fPixel = tex2D(SceneSampler, Input.mUV).rgba; //1Pixel??
    float4 fResut;
    if(L < 1)//100
    {
    //경계면아님
    fResut.x = fPixel.x;
    fResut.y = fPixel.y;
    fResut.z = fPixel.z;
    fResut.w = 1.0f;
    }
    else
    {
    //경계다
    fResut = float4(L.xxx * fColor.rgba, 1);

    }

    이렇게 하니까 바로되더라구요 ;;
    그래서 지금은 툰과 외곽이 다 적용이 됐습니다.

    하지만 여기서 또 궁금한점에 카툰한걸 보면 선이 쭉쭉 나있잖아요? 선을 안보이게 처리하는 방법은 없을까요??

    //아..죄송합니다..제가 설명을 잘못했네요 .. SMD라는 확장자를 가진 캐릭터를 말하는거였습니다 ㅠ
    //예로 ref_Monkey.smd, ani_Monkey.smd...

    답글삭제
    답글
    1. 결을 적게 하면 확티는 안나지만 뭔가 좀 이상하게 보이네요.. 결은 지금 그대로 나두고 뭔가 좀 더 자연스럽게 보이게 하는방법은 없을까요?

      삭제
    2. 안녕하세요 .. 또 이렇게 들리게 되었네요.
      일단 툰은 해결했습니다.. GlowToon으로 했어요..
      그리고.. 또 궁금한게 있어서 이렇게 질문을 또 하게 되네요..

      파티클에 대한건데.. 렌더몽키에서 파티클을 만든다음에 프레임워크에 적용하면
      빛에 막 퍼지는걸로 보이네요.. 렌더몽키에서 Properties - Default로 한거처럼요.
      렌더몽키부분에서도 Default로 하면 이상하게 면으로 보이면서 모양이 안나오고
      Row Major로 바꾸면 제대로 보이네요. 렌더몽키로 파티클(이팩트)을 만들려면 어떤씩으로 해줘야 하나요?

      삭제
    3. 음.. 이런 질문은 스크린샷이 없으면 이해하기가 힘들군요. (빛에 막 파진다는게 무슨 소린지 모르겠어요.. -_-a) 혹시라도 스샷 찍어놓으신거 있으면 좀 보여주시죠.

      삭제
    4. 답변 감사합니다^^ 일단 그 문제는 해결습니다.
      그런데 렌더몽키로 파티클작업하면서 한가지 의문이 생기는게..
      만약에 그래픽팀에서 파티클에 대한 이미지(.tga등등)와 Mesh(.x)를 주면 그걸 렌더몽키에 불러와서 똑같은 모양으로 나오게 할려면 제가 계산하는 방법말곤 없는건가요? 만약 진짜 이런거면 렌더몽키를 사용안하고 하는게 빠를거 같아서요..
      어째 가면갈수록 설명하기가 힘들어지네요.. 이걸 어찌 설명해야할지..ㅠ

      답변 주시면 감사하겠습니다..

      삭제
    5. 아 그리고 현업에서는 파티클작업할때 어떤식으로 하는지 알려주시면 감사하겠습니다...!!!!

      삭제
    6. 설명을 쓰자니 무지 글이 길어질거 같아서..(그리고 귀찮으니 -_-;;;;) 링크로 대신합니다..
      http://kblog.popekim.com/2012/01/06.html

      파티클은 보통 자체 시스템을 만들어서 돌리는게 대부분이죠.. 넵 직접 계산해줘야 합니다..(라고 전 생각합니다.. 뭔가 기발한 다른 방법이 있지 않는한요...)렌더몽키에서 쓰다가 옮기려면 더 힘들거 같네요 ^_^

      삭제
    7. 답글 감사합니다 ~

      삭제