home lecture link bbs blame

◈4. 쉐이더 연습◈

이 장은 앞서 배운 HLSL을 바탕으로 게임에 적용할 만한 쉐이더 코드를 만들어 보는 연습의 장입니다. 정점 쉐이더 보다 픽셀 쉐이더가 양적, 질적으로 가장 많이 게임에서 적용되는 분야이므로 픽셀 처리부분을 집중적으로 연습하겠습니다.

픽셀 쉐이더를 사용하는 부분은 여러 가지가 있겠지만 특히 Post Effect 부분은 같은 장면이라도 전혀 다른 느낌의 연출을 만들 수 있어 게임이 그래픽이 아닌 프로그래머에서 렌더링 품질이 좌우되는 유일한 영역이기도 합니다. 다음 그림을 보면 평범한 장면도 얼마든지 기획의 의도대로 화면을 만들 수 있고 연출에 대한 품질도 개선 시킬 수 있음을 볼 수 있습니다.

 

 

<쉐이더 적용 전, 적용 후(Blur-Glare)>

 

<연필선 효과, 직소>

 

위의 그림은 게임에서 자주 등장하는 흐림(Blur) 효과 또는 Bloom 효과 입니다. 이 것을 스스로 만들 수 있으려면 픽셀 처리가 손에 익어야 합니다. 이 과정에서는 손쉽게 구현할 수 있는 모자이크(Mosaic) 효과를 시작으로 화면 잡음(Noise), 해칭(Hatching) 등을 먼저 연습할 것입니다. 다음으로 흐림(Blur) 효과를 구현할 것입니다. 흐림 효과를 할 수 있다면 포스트 이펙트를 잘 알고 있다는 것이며 이의 연습으로 크로스 효과도 만들어 보겠습니다. 마지막은 외곽선 추출을 이용해서, 수묵화 효과 등 비 실사 렌더링도 사용해 보겠습니다.

4.1 모자이크(Mosaic)

HLSL 이용한 Post Effect의 가장 좋은 연습은 모자이크 효과입니다. 모자이크 효과는 특별한 기술 없이 자와 컴퍼스 연습장 1장만 있으면 기하학적 무늬를 화면 전체에 만들어 낼 수 있습니다.

예를 들어 가며 설명하겠습니다.

 

 

4.1.1 직사각형 모자이크

다음 그림은 화면에 정사각형 패턴을 적용한 것입니다. 이것은 아주 간단한 방법으로 먼저 화면 전체를 실시간 텍스처에 저장을 합니다. 이 텍스처의 UV 좌표를 정수형으로 캐스팅합니다. 그러면 소수점 부분이 사라져 버리게 됩니다. 그런데 바로 캐스팅하면 좌표가 [0, 1] 범위이기 때문에 전부 0이 됩니다. 따라서 정수를 필요한 만큼 곱한 다음 캐스팅하고 이를 다시 곱한 정수만큼 나누는 것입니다.

예를 들어 사각형의 가로 간결을 10으로 하고 싶으면 u 좌표에 10을 곱하고 int 형으로 캐스팅 한 다음 다시 10으로 나눕니다. 그러면 소수정 0.1xxx에서 xxx는 잘려 버리게 됩니다.

 

    

<직사각형 모자이크 효과>

 

다음은 이것을 구현한 쉐이더 코드의 일부 입니다.

 

texture m_TxDif;

sampler smpDif = sampler_state

{

        texture = <m_TxDif>;

        …

};

 

struct SvsOut

{

        float4 Pos:POSITION;

        float2 Tex:TEXCOORD0;

};

float   m_TxD=20;              // Image Mosaic Delta

float4 PxlProc(SvsOut In) : COLOR0

{

        float4 Out=0;

        float   u = In.Tex.x;

        float   v = In.Tex.y;

        int     iX = (int)(u * m_TxD);

        int     iY = (int)(v * m_TxD);

        float   x = iX/(m_TxD);

        float   y = iY/(m_TxD);

 

        Out = tex2D(smpDif, float2(x,y));

        return Out;

}

포스트 이펙트는 앞서 픽셀 쉐이더에서도 보았듯이 다음 그림처럼 장면을 텍스처에 저장하고 이 텍스처에 픽셀 쉐이더를 이용해서 마치 전체 장면에 이펙트를 주는 것처럼 만드는 기술입니다.

앞의 쉐이더 코드를 연습하기 위해서 여러분은 이 쉐이더 코드를 올릴 테스트용 프로그램이 필요합니다. 만약 게임 제작 코드에 이 것들을 넣는 다면 스파게티 코드가 될 것이라는 것은 분명합니다. 시간이 좀 들더라도 다음과 같은 테스트용 프로그램을 준비합니다.

 

   

<쉐이더 테스트용 프로그램: h4_00_screen.zip 에 직사각형 적용 후>

 

h4_00_screen.zipCShaderEx 클래스는 쉐이더 코드를 관리하는 클래스입니다. 실행을 하면 위와 같은 세계지도를 화면에 출력하고 있습니다. 또한 Data 폴더를 보면 위의 그림을 표현 하기 위해 기초 쉐이더 코드들이 있습니다. 이 기초 쉐이더 코드 중에서 가장 중요한 것은 Technique에 있는 함수 컴파일 버전을 픽셀 쉐이더는 "compile ps_2_0" 으로 즉, 2.0이상으로 버전을 정해야 합니다.

 

h4_00_screen.zip "Shader.fx" 파일에 "float m_TxD=20" 부분부터 PxlProc() 함수 안까지 복사해서 붙이고 실행하면 앞의 그림 오른쪽과 같은 그림을 얻을 수 있습니다.

 

간혹 쉐이더 문법에 다음과 같은 코드를 볼 수 있을 때도 있습니다. 이것은 샘플링을 담당하는 샘플러 객체를 register s3에 지정하는 방법입니다. 참고로 s0, s1, s2, … GPU의 쉐이더 레지스터 고유 이름입니다.

 

sampler smp0:register(s3);

 

이렇게 하면 고정 파이프라인에서 [pDevice->SetTexture(3, "텍스처");] 으로 작성한 3번에 연결된 텍스처를 샘플링 합니다. 또한 사용자가 고정 함수 파이프라인에서 지정한 주소 지정 방식(Address Mode)과 필터링(Filtering)등을 그대로 사용할 수 있습니다. 이런 방식은 쉐이더에서 제대로 표현되고 있는지를 고정파이프라인과 거의 동일한 환경으로 만들고자 할 때 주로 작성합니다.

 

위의 구현 후 코드는 h4_01_mosaic0_rect1.zip에 있습니다.

 

다음으로 준비할 것은 실제 장면에 쉐이더 코드를 올려봐야 하므로 지형 정도만 구현되어 있는 다음과 같은 코드를 준비합니다.

 

  

<쉐이더 적용 대상: h4_00_height.zip, h4_00_height_2.zip>

 

또한 장면을 텍스처에 저장하고 화면에 출력할 수 있도록 코드를 만듭니다. 이 때 h4_00_screen.zip 만큼의 코드만 만들어서 실제 장면이 이미지에 출력되는지 코드를 만들어야 합니다. 앞의 오른쪽 그림처럼 나와야 합니다.

 

h4_00_height_2.zipCShaderEx 클래스에는 전체 장면의 픽셀을 전달하는 SetTexture() 함수가 추가되어 있습니다. 또한 전체 장면을 텍스처에 저장하기 위해 고정 함수 파이프라인으로 구현한 "서피스 효과" 장에 있었던 IrenderTarget 객체를 이용했습니다. IrenderTarget 객체 방법은 "서피스 효과" 부분을 다시 읽어 보기 바랍니다.

 

이렇게 해서 우리는 쉐이더 테스트용 프로그램, 장면에 적용해 볼 수 있는 프로그램 두 가지를 준비했습니다. 앞으로 모든 포스트 이펙트에 대한 코드는 쉐이더 테스트용 프로그램에서 먼저 실행해보고, 이후 게임 화면과 유사한 환경에서 이를 다시 실행해 보겠습니다.

 

 

4.1.2 마름모형 모자이크

 직사각형은 너무나 쉬워서 손이 아직 덜 풀렸을 것입니다. 다음으로 직사각형 보다 약간 난이도 있는 마름모를 적용해 봅시다. 마름모는 의외의 난이도가 있어서 연습장에 한 번 정도는 그려봐야 합니다. (기하학 문제는 그림을 잘 그리면 쉬워집니다.)

 

<마름모형 UV인덱스>

 

그림처럼 먼저 4 구역 A, B, C, D로 설정합니다. 자세히 보면 마름모로 색상을 칠하려면 중심 되는 지점을 먼저 찾아야 되는데 (0,0), (2,0), …, (1,1), (3,1), …, X+Y가 짝수 인 경우가 중심이 됩니다.

또한 각 점에 대해서 앞서 직사각형에서처럼 곱하기à캐스팅으로 해서 해당 인덱스를 먼저 구합니다. 이 인덱스가 중심인지 아닌지는 modular 연산자 "%"를 이용합니다. "인덱스%2" 하면 짝수, 홀수 판정이 나오고 둘 다 (0,0) 또는 (1,1)의 경우에는 인덱스가 중심 좌표가 됩니다.

 

또한 인덱스에서 거리 = 자신위치- 중심위치로 , 를 구합니다. 이것을 가지고 완전한 중심 인덱스를 찾습니다.

예를 들어 점 p1, p2는 인덱스가(0,0) 입니다. 이들의 중심점은 변동 없이 (0,0)을 처음에 정한 인덱스를 중심 인덱스로 합니다. p1의 경우는 가 됩니다. 그런데 점 p2이 됩니다. 따라서 점 p2x 방향으로 인덱스 x + 1, y 방향으로 인덱스 y + 1로 각각 1만큼 올립니다.

 

이번에는 점 p3를 봅시다. 이 점의 인덱스는 (1,0)을 이 점은 중심 점이 아닙니다. 따라서 x 방향 또는 y 방향으로 중심을 이동해야 하는데 x 방향으로는 앞서 구한 방법대로 를 그대로 구하고 y 방향은 1을 더한 만큼에서 를 구합니다. 만약 이면 y 방향으로 1만큼 증가한 값이 중심 인덱스이고 그렇지 않으면 x 방향으로 1만큼 올린 값이 중심인덱스가 됩니다.

이와 같은 방법으로 다음과 같은 코드를 만들어 낼 수 있습니다.

 

float4 PxlProc(SvsOut In) : COLOR0

        int     mX = nX%2;

int     mY = nY%2;

 

        float DelX = 0; float DelY = 0; float Del = 0;

 

        if( (0==mX && 0== mY) || (1==mX && 1== mY))

        {

               DelX = TxX - nX;

               DelY = TxY - nY;

               Del = DelX + DelY;

 

               if(Del<1)

               {

                       U = nX;    V = nY;

               }

               else

               {

                       U = nX + 1.f;

V = nY + 1.f;

               }

        }

        else

        {

               DelX = TxX - nX;

               DelY = nY+1 - TxY;

               Del = DelX + DelY;

 

               if(Del<1)

               {

                       U = nX;

V = nY + 1.f;

               }

               else

               {

                       U = nX + 1.f;

V = nY;

               }

        }

 

        U /= m_fRpt;

V /= m_fRpt;

 

        Out = tex2D(smpDif, float2(U, V));

        return Out;

}

 

<마름모: h4_01_mosaic1_lozenge.zip>

<마름모: h4_01_mosaic1_lozenge_a.zip>


 

전체 코드는 h4_01_mosaic1_lozenge.zip 또는 h4_01_mosaic1_lozenge_a.zip "data" 폴더의 "shader.fx" 파일을 참고 하기 바랍니다.

 

 

4.1.3 은행잎 모자이크 1

마름모 형태는 쉐이더 코드보다 기하학적 패턴 만들기에 더 노력이 들었습니다. 지금 하고자 하는 은행잎 모양도 마름모와 비슷하게 코드를 만들기 보다는 그림을 그리고 해석하는 데 시간이 더 많이 듭니다. 이런 패턴 만드는 일을 계속 하다 보면 포스트 이펙트에서 픽셀 쉐이더의 역할이 선명해 질것입니다.

 

<은행잎 패턴>

 

그림을 보면 x,y인덱스의 범위를 [0, 1]로 만들 때 이 정사각형 안의 u, v위치는 황색 점 A, B, C, D들 중 하나에서 샘플링 해야 합니다. 가장 편하게 만들 수 있는 코드는 점들을 돌아가면서 샘플링 하되 거리를 비교해서 거리가 0.5(1의 반지름) 안에 있을 때만 샘플링 하는 것입니다.

 

예를 들어 점 p1 A와 거리를 구합니다. 구한 거리는 그림에서 보듯 0.5가 안되어 샘플링을 합니다.  다음 점 B와 비교하면 거리가 0.5보다 작습니다. B의 위치에서 샘플링하고 이전 값을 바꿉니다. 다음 점 C와 비교해보면 거리가 0.5를 넘게 되어 C에서는 샘플링을 안 합니다. 마지막 D도 마찬가지로 거리가 0.5보다 커서 샘플링을 안 합니다. 이렇게 되면 p1의 색상은 점 A가 됩니다. 또 다른 점 p2를 봅시다. p2 A, B 점에 대해서는 거리가 0.5보다 커서 샘플링을 안 합니다. C와 거리는 0.5 미만이므로 샘플링을 합니다. 그런데 점 D와도 거리가 0.5 미만이라 최종 점 D에서 샘플링 한 색상을 최종 색상으로 정합니다.

 

코드의 구현에서는 거리로 비교하지 않고 거리의 제곱으로 비교합니다. 따라서 0.5가 아닌 0.25로 비교합니다. 또한 색상은 어떤 식으로든 결정이 되어야 하므로 최초 점 A에서 무조건 샘플링을 하게 합니다. 이렇게 되면 중간에 색상을 잃어버리는 일을 없어 집니다.

 

이 내용을 가지고 쉐이더 코드를 구현하면 다음과 같습니다.

 

float   m_fRpt=20;             // Image Repeat

float4 PxlProc(SvsOut In) : COLOR0

        float2  Tx = In.Tex * m_fRpt;

        float2  Del=0;

        float2  t=0;

 

        int     iX = (int)(In.Tex.x * m_fRpt);

        int     iY = (int)(In.Tex.y * m_fRpt);

 

        // 처음 시작(0.5, 0)

        t  = float2(0.5 + iX, 0 + iY);

        t /= m_fRpt;

        Out = tex2D( smpDif, t);

 

        // (0, 0.5)

        t   = float2(0.0 + iX, 0.5 + iY);

        Del = Tx - t;

 

        if( Del.x*Del.x + Del.y*Del.y<=0.25)

        {

               t /= m_fRpt;

               Out = tex2D( smpDif, t);

        }

 

        // (1, 0.5)

        t   = float2(1.0 + iX, 0.5 + iY);

        Del = Tx - t;

 

        if( Del.x*Del.x + Del.y*Del.y<=0.25)

        {

               t /= m_fRpt;

               Out = tex2D( smpDif, t);

        }

 

        // (0.5, 1)

        t   = float2(0.5 + iX, 1. + iY);

        Del = Tx - t;

 

        if( Del.x*Del.x + Del.y*Del.y<=0.25)

        {

               t /= m_fRpt;

               Out = tex2D( smpDif, t);

        }

 

        return Out;

}

 

<은행잎 패턴: h4_01_mosaic1_ginkgo.zip>

<은행잎 패턴: h4_01_mosaic1_ginkgo_a.zip>

 

전체 코드는 h4_01_mosaic1_ginkgo.zip, h4_01_mosaic1_ginkgo_a.zip을 참고하기 바랍니다.

 

 

4.1.4 은행잎 모자이크 2

직사각형, 마름모, 은행잎 형태는 손으로 계산할 수 있어서 프로그램이 가능합니다. 그런데 이렇게 직접 패턴에 대한 구현을 쉐이더 코드로 만드는 것은 많은 부담이 아닐 수 없습니다. 만약 그래픽으로 만든 패턴이 있으면 프로그램은 패턴 이미지에 저장된 픽셀을 새로운 샘플링 좌표 u, v 로 이 u, v로 바꾸고 화면에 출력할 텍스처의 픽셀을 샘플링 한다면 프로그램은 상당히 간소화 될 것이라는 것은 분명합니다.

 

다음 그림은 그래픽 패턴입니다.

 

     

<그래픽으로 작성한 패턴>

 

이 그래픽 패턴 중에서 가장 왼쪽에 있는 사각형을 보기 바랍니다. 이 사각형을 자세히 보면 다음과 같이 색상이 분포되어 있음을 할 수 있습니다.

 

<패턴 맵1>

 

왼쪽 상단은 RGB(255,255,255)입니다. 쉐이더에서 색상은 [0, 1] 범위로 사용하고 있으므로 이 범위 값으로 색상으로 바꾸면 RGB(1, 1, 1)이 됩니다. 마찬가지로 우측하단도 RGB(0, 0, 255) RGB(0, 0, 1)이 됩니다. 만약 그림의 갈색 점이나 초록색 점을 얻은 색상을 u, v로 사용한다면 이들 점은 RGB(128, 128, 255)è RGB(0.5, 0.5, 1)로 옮겨야 정확한 중심이 됩니다.

 

초록 점 또는 갈색 점의 색상 R, G Pattern(r, g) 라 하면 중심 색으로 옮기는 이동 크기는 Pattern( r-0.5, g-0.5)가 됩니다.

따라서 새로운 U,V 샘플링 좌표는 다음과 같이 결정할 수 있습니다.

 

Old(U, V) = 기존(U, V)

New(U, V) = Old(U, V) + 중심점으로 이동

= Old(U, V) + Pattern( r-0.5, g-0.5)

 

만약 반복 횟수가 있으면 이 반복 횟수만큼 늘린 다음 줄입니다.

 

New.U     = (Old.U * RepeatX - 0.5)/RepeatX

New.V     = (Old.V * RepeatY - 0.5)/RepeatY

 

이에 대한 쉐이더 구현은 h4_01_mosaic2_ginkgo.zip "Shader.fx"에 구현되어있습니다.

 

texture m_TxPtn;

sampler smpPtn = sampler_state

{

        texture = <m_TxPtn>;

};

 

static float   fRepeatX = 4;

static float   fRepeatY = 3;

float   m_fRpt=4;              // Image Repeat

 

float4 PxlProc(SvsOut In) : COLOR0

{

        float2  TxOld = 0;

        float2  TxNew = 0;

 

        fRepeatX *= m_fRpt;

        fRepeatY *= m_fRpt;

 

        TxOld.x = In.Tex.x * fRepeatX;

        TxOld.y = In.Tex.y * fRepeatY;

 

        t1 = tex2D(smpPtn, TxOld);

 

        // 텍스처 좌표를 이동

        TxNew.x = (t1.x - 0.5f)/fRepeatX;

        TxNew.y = (t1.y - 0.5f)/fRepeatY;

 

        TxNew += In.Tex;

 

        t0 = tex2D(smpDif, TxNew);

 

        if(t1.x>0.99 && t1.y >0.99)

               Out= 1;

        else

               Out = t0;

 

        return Out;

}

 

패턴 텍스처의 색상을 가져오기 위해서 sampler smpPtn가 추가 되었습니다. 또한 패턴의 크기가 화면의 가로 세로 비율을 맞추기 위해 X 방향, Y 방향으로 4, 3을 반복 회수에 곱해서 사용합니다.

다음 그림은 이 패턴 맵을 적용한 화면입니다.

 

 

 

 

<패턴 맵2 - 쉐이더 시험 화면>

 

 

 

 

<패턴 맵3 - 게임 화면>

 

만약 두 개의 장면을 교차하는 연출을 만들 때 이 패턴 반복 횟수를 동적으로 변화서 적용해 볼 수 있습니다. 이것을 실제 화면에 적용해 보면 좋겠지만 여건상 여기서는 그런 것이 있다고 가정하고 하나의 화면에 대해서만 적용하면 다음과 같은 화면을 얻을 수 있습니다.

 

 

<반복 횟수를 변환한 패턴: h4_01_mosaic2_hexa.zip, h4_01_mosaic2_hexa_a.zip>

 

 

4.1.5 직소(Jigsaw) 퍼즐

패턴의 이미지는 타일처럼 색상의 변화가 연속이어야 합니다. 또한 그 색상들은 중심 색을 정확하게 샘플링 할 수 있어야 합니다. 지금까지 화면에서 사용한 이미지 패턴들은 대칭이 존재해서 만들기가 어렵지 않았습니다. 그런데 직소는 대칭보다 비대칭에 가깝습니다. 따라서 이전에 해왔던 작업보다 노력이 많이 필요한데 여러분들도 이와 비슷한 것을 주제 삼아 만들어 보기 바랍니다.

 

처음에는 화가 에셔(M.C. Escher)의 그림을 모티브로 비대칭 패턴을 만들려고 했으나 실패하고 간단한(사실 간단하지 않은) 직소를 재미 삼아 도전해 보았습니다. 다음 그림은 직소원본과 이 그림을 연속으로 배치 했을 때의 모습입니다.

 

 

<직소 패턴>

 

이 직소 모양도 쉐이더 코드는 이전과 거의 같습니다. 단지 차이는 전에는 사각형의 패턴의 중심 색을 샘플링 하도록 했지만 이 직소는 x 방향의 4/5 되는 지점에서는 오른쪽 방향으로 0.25만큼 더 이동해야 합니다. y 방향은 1/5 되는 지점에서 위로 0.5만큼 더 이동해야 합니다.

 

이것을 의사 코드(pseudo-code) 로 만들면

 

샘플링 텍스처 좌표 t(x, y) = Pattern( 원본(U ,V))

새로운 좌표 U = t.x * 1.25 - 0.75

새로운 좌표 V = t.y * 1.25 - 0.50

 

이 됩니다.

예제의 "shader.fx" 파일에는 반복 횟수까지 고려해서 다음과 같이 구현되어 있습니다.

 

float4 PxlProc(SvsOut In) : COLOR0

        TxOld.x = In.Tex.x * fRepeatX;

        TxOld.y = In.Tex.y * fRepeatY;

 

        t1 = tex2D(smpPtn, TxOld);

 

        // 텍스처 좌표를 이동

        TxNew.x = (t1.x * 1.25f - 0.75f)/fRepeatX;

        TxNew.y = (t1.y * 1.25f - 0.50f)/fRepeatY;

 

        TxNew += In.Tex;

 

        t0 = tex2D(smpDif, TxNew);

 

다음 두 그림은 직소 패턴을 이용해서 화면 입니다.

 

 

<직소 패턴: h4_01_mosaic2_jigsaw.zip, h4_01_mosaic2_jigsaw_a.zip>

 

 

4.1.6 Voronoi Diagram

지금까지 만든 모든 패턴을 적용해서 만든 포스트 이펙트는 Non Reality에 가깝습니다. 이것을 3D에서 NPR(Non Realistic Rendering)이라 합니다. NPR 중에서 화면 자체를 유화(Oil Paint), 종이 찢어 붙이기, 중세 교회의 모자이크, 스테인드 글라스(Stained Glass) 등과 같은 효과를 만들기 위해서 가장 필요한 것이 화면의 영역을 분할 하는 것입니다. 이 때 보로노이 도형(Voronoi Diagram)을 이용합니다.

이 도형은 다음 그림처럼 인접한 점들과 정확히 절반 되는 위치에서 수직선을 그어 이 선들이 만나는 지점들을 꼭지점으로 다각형 입니다. 보로노이 도형을 만들면 들로네 삼각형(Delaunay Triangle)을 구성할 수 있어서 정점을 가지고 적절한 삼각형 메시를 구성하는 데 사용됩니다.

보로노이가 영역에 대한 문제이다 보니 지형, 사회적인 서비스 등에서도 많이 이용되는 도형이니 자료를 찾아 보기 바랍니다.

이 도형을 Adobe Photoshop 에서도 볼 수 있는데 이미지를 열고 Filter à Pixelate à Crystallize를 선택하면 이 도형으로 만들어진 이미지를 얻을 수 있습니다.

 

 

보통 보로노이 도형은 가장 왼쪽에 있는 점부터 이 점에 가장 가까운 점들부터 수직선을 그어나가면서 선들끼리 만나는 점을 계속 갱신해가면서 맨 오른쪽의 마지막 점까지 이를 진행 합니다.

그런데 여기서 우리는 색상을 보로노이 도형에 맞게 채우는 것이 목적이므로 이 알고리즘을 이용하지 않고 다음과 같이 원뿔을 이용할 것입니다.

 

보로노이 도형을 위해서 보지 않고 옆에서 보면 수직선은 적당히 큰 반경을 가진 원뿔의 경계가 됨을 쉽게 알 수 있습니다.

 

<보로노이 도형: 원뿔의 인접 면으로 해석>

 

즉 처음부터 색상을 가진 3차원 원뿔을 임의로 화면에 연출하면 색상이 채워진 보로노이 도형을 만들 수 있게 됩니다.

이 때 원뿔의 색상은 화면을 저장한 픽셀에서 가져와야 하는데 원뿔의 위치를 픽셀의 U,V로 고정시키는 것입니다. 이렇게 되면 원뿔의 위치로 구한 픽셀의 색상이 하나의 원뿔 전체에 같은 색으로 결정이 될 것입니다. 따라서 쉐이더 코드는 정점의 텍스처 좌표에서 샘플링이 아닌 외부에서 주어진 위치에 의해 샘플링 되도록 다음과 같이 만들어야 합니다.

 

uniform float2 m_UV;

 

float4 PxlProc(SvsOut In) : COLOR0

{

        float4  Out=0;

        float4  t0=0;

 

        t0 = tex2D(smpDif, m_UV);

        Out = t0;

        return Out;

}

 

복잡할 것 같았는데 코드가 단순하죠?

대신 렌더링에서는 다음과 같이 원뿔들을 보로노이 도형을 구성하는 점들만큼 렌더링 해야 합니다.

 

void CShaderEx::Render()

        m_pEft->SetTexture("m_TxDif", m_pTex);

 

        for(int i=0; i<m_nCon; ++i)

        {

               // 위치는 [-1.1] 이었으므로 범위를 [0,1]한다.

               uv = ( m_pPos[i] + D3DXVECTOR4(1,1,0,0) ) * 0.5;

               uv.y = 1 - uv.y;

 

               m_pEft->SetVector("m_UV", &uv);

 

                mtI._41 = m_pPos[i].x;

               mtI._42 = m_pPos[i].y;

 

               m_pDev->SetTransform(D3DTS_WORLD, &mtI);

               m_pCon->DrawSubset(0);

        }

 

다음 그림은 보로노이 도형을 출력한 화면입니다.

 

 

<보로노이 도형: h4_01_mosaic3_voronoi.zip, h4_01_mosaic3_voronoi_a.zip>

 

과제) 게임에서 적용 될 수 있는 보로노이 도형과 NPR에 대한 자료를 정리해 오시오.

 

 

4.2 화면 섭동 (Perturbation)

빛은 방안으로 들어와도 안쪽을 보지 못하게 특정한 형태로 울퉁불퉁한 모습한 무늬 유리(Embossed Glass)를 본 적이 있을 것입니다. 또한 뜨거운 여름의 아스팔트 위, 장작 불의 열기, 봄철 기온차이로 아지랑이 효과도 본 적이 있을 것입니다. 이러한 자연 현상은 빛의 굴절에 의해 발생합니다. 그런데 이것을 3D로 해석을 하면 전체 장면을 구성하는 픽셀을 특정한 형태를 통해서 움직이는 것으로 볼 수 있습니다. 또한 픽셀을 움직이게 한다는 것은 샘플링의 U, V 좌표를 변동하는 것과 같은 의미가 됩니다.

이 장에서는 무늬 유리, 파티클(Particle)을 이용해서 이러한 효과를 구현해 보겠습니다.

 

 

4.2.1 무늬 유리 효과(Embossed Glass) 1

무늬 유리는 일정한 무늬가 올록볼록 한 형태로 배치되어 있는 유리입니다. 이 형태를 픽셀의 움직이게 하는 요소로 본다면 우리는 무늬 유리를 3D로 구현하기 위해서 전체 장면의 픽셀을 움직일 다음과 같은 이미지를 생각할 수 있습니다.

 

<Embossing 텍스처>

 

그런데 이 올록볼록한 텍스처 이미지를 어떻게 구성할 것인가 잘 생각해 보면 이전에 원의 형태로 은행잎을 만들 때 쓰던 텍스처를 이용해서 만들 수 있음을 알 수 있습니다. , Embossing 텍스처를 실시간으로 먼저 만들고 이 Embossing 텍스처에 원을 그릴 때 사용하던 이미지를 적당한 크기와 간격으로 렌더링 하면 Embossing 텍스처는 우리가 원하는 형태의 무늬 유리가 됩니다.

그리고 이 Embossing 텍스처를 앞서 패턴을 사용할 때처럼 픽셀을 움직이게 하는 용도로 사용하면 우리는 전체 장면에 무늬 유리 효과를 만들어 낼 수 있게 됩니다.

 

h4_02_emboss0.zip INT CShaderEx::Restore() 함수에는 다음과 같이 Embossing 텍스처를 만드는 코드가 있습니다.

 

IrenderTarget* m_pTexP;               // Embossing Texture

m_pTexP->BeginScene();

 

m_pDev->Clear(0, NULL, D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER, 0XFF8080FF, 1, 0);

m_pDev->SetRenderState(D3DRS_ALPHABLENDENABLE, TRUE);

m_pDev->SetRenderState(D3DRS_ZENABLE, FALSE);

m_pDev->SetFVF(D3DFVF_XYZRHW | D3DFVF_TEX1);

 

m_fW = 12;

 

struct T

{

D3DXVECTOR4    p;

D3DXVECTOR2    t;

};

 

T pVtx[4];

 

pVtx[0].p = D3DXVECTOR4(0,0,0,1);

pVtx[1].p = D3DXVECTOR4(0,0,0,1);

pVtx[2].p = D3DXVECTOR4(0,0,0,1);

pVtx[3].p = D3DXVECTOR4(0,0,0,1);

 

pVtx[0].t = D3DXVECTOR2(0,0);

pVtx[1].t = D3DXVECTOR2(1,0);

pVtx[2].t = D3DXVECTOR2(1,1);

pVtx[3].t = D3DXVECTOR2(0,1);

 

INT i, j;

for(j=0; j<31; ++j)

{

        for(i=0; i<41; ++i)

        {

               pVtx[0].p.x = i*10 - m_fW;

               pVtx[0].p.y = j*10 - m_fW;

 

               pVtx[1].p.x = pVtx[0].p.x + m_fW;

               pVtx[1].p.y = pVtx[0].p.y + 0 ;

 

               pVtx[2].p.x = pVtx[0].p.x + m_fW;

               pVtx[2].p.y = pVtx[0].p.y + m_fW;

 

               pVtx[3].p.x = pVtx[0].p.x + 0 ;

               pVtx[3].p.y = pVtx[0].p.y + m_fW;

 

                pVtx[0].p *= 2; pVtx[1].p *= 2;

               pVtx[2].p *= 2;        pVtx[3].p *= 2;

 

               m_pDev->SetTexture(0, m_pTex1);

               m_pDev->DrawPrimitiveUP(D3DPT_TRIANGLEFAN, 2, pVtx, sizeof(T));

        }

}

m_pTexP->EndScene();

 

다 아는 내용이라 간단히 설명 생략하겠습니다. Clear() 함수에서 0XFF8080FF 값은 RGBA(0.5, 0.5, 1, 1)에 해당합니다. 즉 픽셀의 움직임을 0으로 한 것입니다. 중간에 구조체를 선언하고 정점 4개의 U, V를 정하고 for 문을 돌면서 위치만 설정해서 렌더링 하는 부분은 Embossing 형태를 만드는 것입니다. 디바이스의 SetTexture() 함수에 사용한 텍스처는 원을 패턴으로 그릴 때 사용한 텍스처 입니다.

쉐이더 코드는 다음과 같이 처음 패턴을 시작할 때의 모습 그대로 돌아왔습니다.

 

float m_fWidth;

 

float4 PxlProc(SvsOut In) : COLOR0

{

        float4  Out=0;

        float4  t0=0;

        float4  t1=0;

 

        float2  TxOld = 0;

        float2  TxNew = 0;

 

        TxOld.x = In.Tex.x;

        TxOld.y = In.Tex.y;

 

        t1 = tex2D(smpPtn, TxOld);

 

        // 텍스처 좌표를 이동

        TxNew.x = (t1.x - 0.5f)/m_fWidth;

        TxNew.y = (t1.y - 0.5f)/m_fWidth * 4./3;

 

        TxNew += In.Tex;

 

        t0 = tex2D(smpDif, TxNew);

 

        Out = t0;

        return Out;

}

 

실행하면 다음과 같이 화면에 출력되는데 오른쪽 그림은 좀 더 촘촘히 Embossing텍스처를 만든 것입니다.

 

 

<Embossing 텍스처를 적용한 화면: h4_02_emboss0.zip>

 

void CShaderEx::Render()함수에서 #if 0 #if 1로 하고 컴파일 한 다음 실행하면  <Embossing 텍스처>가 화면에 출력 됩니다.

 

이것을 지형에 적용시켜 보면 다음과 같이 출력됩니다.

 

 

< Embossing 텍스처를 적용한 화면: h4_02_emboss0_a.zip

 

 

4.2.1 무늬 유리 효과(Embossed Glass) 2

무늬 유리 효과를 만들었는데 같은 간격으로 일정하게 나타나는 것이 좀 단조로운 느낌이 듭니다.

이 번에는 circle 이미지의 위치를 Random 하게 만들어서 Embossing 텍스처를 만들어 봅시다.

수정할 부분은 for 문 내용으로 다음과 같이 위치를 rand()함수로 정합니다.

 

for(int i=0; i<20000; ++i)

{

        pVtx[0].p.x = rand()%900 *1.4f - m_fW;

        pVtx[0].p.y = rand()%700 *1.4f - m_fW;

 

        pVtx[1].p.x = pVtx[0].p.x + m_fW;

        pVtx[1].p.y = pVtx[0].p.y + 0 ;

        pVtx[2].p.x = pVtx[0].p.x + m_fW;

        pVtx[2].p.y = pVtx[0].p.y + m_fW;

        pVtx[3].p.x = pVtx[0].p.x + 0 ;

        pVtx[3].p.y = pVtx[0].p.y + m_fW;

 

        m_pDev->SetTexture(0, m_pTex1);

        m_pDev->DrawPrimitiveUP(D3DPT_TRIANGLEFAN, 2, pVtx, sizeof(T));

}

 

20000 이란 숫자는 큰 의미는 없습니다. 겹치는 부분이 있을 것 같아 적당히 큰 값을 선택했습니다. 이것을 실행하면 다음과 같습니다.

 

 

<Random() 함수를 사용한 무늬 유리 효과: h4_02_emboss1.zip, h4_02_emboss1_a.zip>

 

이전의 같은 간격으로 구성한 것보다 많이 좋아 졌음을 볼 수 있습니다. 여기에 작은 무늬의 종류를 이전에 사용했던 패턴들을 섞어봅시다.

이전 코드와 달라진 것은 작은 무늬를 더 추가한 것과

 

if(FAILED(D3DXCreateTextureFromFile(m_pDev, "Texture/pattern_circle.png", &m_pTex1)))

        return -1;

 

if(FAILED(D3DXCreateTextureFromFile(m_pDev, "Texture/pattern_ginkgo.png", &m_pTex2)))

        return -1;

 

if(FAILED(D3DXCreateTextureFromFile(m_pDev, "Texture/pattern_cube1.png", &m_pTex3)))

        return -1;

 

무늬 유리창을 만드는 For문에서 rand() 함수를 사용해 패턴을 선택하는 것 입니다.

 

for(int i=0; i<20000; ++i)

{

        int c = rand()%3;

 

        if(0 ==c)      m_pDev->SetTexture(0, m_pTex1);

        else if(1 ==c) m_pDev->SetTexture(0, m_pTex2);

        else if(2 ==c) m_pDev->SetTexture(0, m_pTex3);

 

        m_pDev->DrawPrimitiveUP(D3DPT_TRIANGLEFAN, 2, pVtx, sizeof(T));

}

 

 

<무늬 유리창 효과: h4_02_emboss1.zip>

 

 

<무늬 유리창 효과: h4_02_emboss1_a.zip>

 

하나의 패턴을 사용해서 만드는 것보다 여러 개를 혼합해서 사용하는 것이 더 효과적이라는 것을 알 수 있습니다.

 

 

4.2.2 화면 잡음(Noise)

아날로그 TV의 경우 전파 신호가 약하면 잡음(Noise)의 효과가 강해져 화면이 좌우로 일그러지는 현상을 볼 수 있습니다. 이런 종류의 Noise White Noise 라고도 합니다. 이 효과도 가만히 생각해보면 정상적인 3D 화면 픽셀을 좌우로 이동시키는 것과 유사합니다. 그런데 이 이동 값은 일정한 것이 아니라 거의 Random 한 것입니다. (잡음 또한 Random입니다.)

 

<노이즈 맵>

만약 Random을 표현할 수 있는 텍스처가 있다면 우리는 바로 앞에서 해왔던 일들을 거의 그대로 적용할 수 있습니다. 간단히 정리하면 Random 이미지에서 픽셀을 가지고 와서 이 값을 정상 화면의 UV를 왜곡시키는 요소(변수)로 만들면 영상 잡음을 만들어 낼 수 있을 것입니다.

Radom이미지는 Adobe Photoshop 과 같은 그래픽 툴에서 Noise를 통해서 다음과 같이 만들 수 있습니다. 이것을 노이즈 맵(Noise Map)이라 부르기도 합니다.

 

이 노이즈 맵의 색상(R, G, B) 중에 R, G를 화면 왜곡 비율로 설정하면 되는데 색상은 [0, 1] 범위 이고 U, V [0, 1] 범위이므로 색상의 R, G값을 그대로 사용하지 않고 적당히 줄입니다.

이 값을 원본 U, V에 더하면 정상 화면을 왜곡 시키는 좌표가 될 것입니다.

 

다음은 쉐이더 코드입니다.

 

float   m_fRepeatX = 10;              // Repeat for X

float   m_fRepeatY = 10;              // Repeat for Y

 

float4 PxlProc0(SvsOut In) : COLOR0

{

        float4 Out=0;

        float4 t0= 0;

        float2 t= 0;

 

        t = In.Tex;

        t.x *= m_fRepeatX;

        t.y *= m_fRepeatY;

        t0= tex2D(smp1, t)-0.5;               // Sampling from Noise Texture ==> [-0.5, 0.5];

        t0 *=0.1f;                    // [-0.05, 0.05];

        t = In.Tex + float2(t0.x, t0.y);      // Modified Texture Coordinate

        t0= tex2D(smp0, t);                   // Sampling from Diffuse Texture

 

        Out     = t0;

        return Out;

}

 

코드를 보면 정상화면의 U V에 노이즈 맵의 반복 정도를 X Y에 다르게 곱하고 있습니다. 이것은 화면의 가로 세로 방향에 대한 노이즈를 다른 비율로 만들기 위해서 입니다.

이 곱한 값을 가지고 노이즈 맵(smp1)에서 색상을 가져온 다음 0.5를 빼서 노이즈가 좌우 또는 상하로 발생하도록 합니다. 그 다음 0.1를 곱해서 적당히 왜곡이 너무 크지 않도록 줄입니다. 이 왜곡 값과 원본 U, V값을 더해 최종 텍스처 좌표로 만든 다음 화면을 저장한 텍스처에서 샘플링 합니다.

 

반복 횟수 값을 다음과 같이 설정하면 화면에서 잡음의 정도를 볼 수 있습니다.

float   time = D3DXToRadian( GetTickCount() *0.05f);

float   fSin=sinf(time);

float   fRepeatX = 1 + (1+ fSin)*10;

float   fRepeatY = 1 + (1+ fSin)*10;

m_pEft->SetFloat("m_fRepeatX", fRepeatX);

m_pEft->SetFloat("m_fRepeatY", fRepeatY);

 

 

<화면 잡음 효과: h4_02_noise.zip, h4_02_noise_a.zip>

 

만약 반복 값 다음과 같이 Random을 적용하면 상하 좌우로 심하게 흔들리는 화면을 얻을 수 있습니다.

 

fRepeatX = (10 + (rand()%100)) * 0.1f;

fRepeatY = (10 + (rand()%100)) * 0.1f;

 

그런데 적당히 좌우로만 잡음이 생기도록 하는 것이 좋으므로 Y방향은 고정하고 X 방향만 Radom을 적용합니다.

 

fRepeatX = (1 + rand()%51)/20;

fRepeatY = 20; // Y는 적당한 값으로 고정

 

 

<화면 잡음 효과: h4_02_noise_a.zip>

 

 

4.3 연필 선 효과(Pencil Stroke)

화면을 연필로 스케치한 것처럼 표현하는 방법은 여러 가지가 있습니다. 첫 번째 방법은 3D 모델을 렌더링 할 때 반사의 밝기에 따라 스케치용 텍스처를 매핑 하는 방법이 있고, 두 번째 방법은 색상을 R, G, B로 분리해서 각각의 채도, 명도에 적용하는 방법, 그리고 세 번째 방법은 전체 장면을 각 픽셀의 밝기에 따라 레벨을 정한 텍스처를 혼합하는 방법이 있습니다.

이 장에서는 세 번째 방법인 장면의 밝기에 연필 스케치 이미지를 적용하는 방법을 보이겠습니다.

 

<연필선 이미지 A>

<연필선 이미지 B>

<연필선 이미지 C>

 

세 번째 방법을 적용하기 위해서 그림처럼 연필선 이미지 A, B, C를 준비합니다. 보면 알다시피 A가 가장 밝고 그 다음 B, 마지막은 C인 것을 알 수 있습니다.

 

이 이미지를 장면에 적용하기 위해서 연필선 이미지 A와 연필 선 이미지 B만 혼합하는 예를 먼저 들어 봅시다.

만약 연필선 이미지 A 60%, 연필선 이미지 B 40%를 혼합할 때 어떻게 하면 될까요? 당연히 A* 0.6 + B * 0.4 하면 됩니다. 이 수식은 선형보간과 다름이 없습니다. 따라서 최종 색상은 = A *0.6 + (1-0.6) * B 한 것과 다름이 없습니다.

그런데 이렇게 쉬운 내용을 쉐이더 프로그램으로 표현할 줄 알아야 합니다.

 

의사 코드로 대충 만들어 보면 다음과 같습니다.

 

float  W  = 0.6;

float4 tA = Sampling(Texture A);

float4 tB = Sampling(Texture B);

 

float4 Out = tA * W + (1 - W) * tB;

 

이것을 연습장에 작성하신 분은 이미 나머지 내용도 쉽게 만들어 갈 수 있을 것입니다.

만약 W를 장면 픽셀의 밝기로 한다면 앞의 의사 코드는 쉐이더를 이용해서 우리가 연필 선으로 표현하고자 하는 코드와 동일하게 됩니다.

남아 있는 것은 픽셀의 밝기입니다. 이것은 간단하게 흑백 이미지로 만들어서 이 크기를 픽셀의 밝기로 다음과 같이 정하면 됩니다.

 

픽셀의 밝기 W = (픽셀 Red)*0.301 + (픽셀 Green) * 0.587 + (픽셀 Blue) * 0.114

 

이제 이것을 쉐이더로 표현하면 다음과 같습니다.

 

sampler smp0 : register(s0);

sampler smp1 : register(s1);

sampler smp2 : register(s2);

 

float4 PxlProc0(float2 uv : TEXCOORD0) : COLOR0

{

        float2  tx=  uv * 8.0f;               // U,V 좌표를 8배 한다.

 

        float4 t0 = tex2D(smp0, uv);

        float4 t1 = tex2D(smp1, tx);

        float4 t2 = tex2D(smp2, tx);

 

        float4 Out =0;

        float  fMono = t0.r * 0.299 + t0.g * 0.587 + t0.b * 0.114;

 

        float fW = fMono;

        Out = fW * t1 + (1-fW) *t2;

 

        return Out;

}

 

쉐이더 코드에서 연필선 효과가 잘 안보여 UV좌표를 8배 했습니다. 또한 쉐이더 코드에서 샘플러 register를 지정해서 사용하고 있어서 프로그램에서는 Address Mode와 필터링을 지정해야 하고 디바이스의 SetTexture() 함수로 register에 텍스처를 전달해야 합니다.

 

for(int i=0; i<4; ++i)

{

        m_pDev->SetSamplerState(i, D3DSAMP_ADDRESSU, D3DTADDRESS_WRAP);

        m_pDev->SetSamplerState(i, D3DSAMP_MINFILTER, D3DTEXF_LINEAR);

}

m_pEft->SetTechnique("Tech");

m_pDev->SetTexture(0, m_pTx0);

m_pDev->SetTexture(1, m_pTx1);

m_pDev->SetTexture(2, m_pTx2);

m_pDev->DrawPrimitiveUP( D3DPT_TRIANGLEFAN, 2, m_pVtx, sizeof(VtxDUV1));

 

실행파일에서는 이미지를 로드하고 쉐이더로 연결한 것 밖에 없습니다. 이것을 실행 하면 다음의 왼쪽과 같은 어렴풋한 윤곽이 잡힌 장면을 볼 수 있습니다.

 

 

<연필 선 효과: h4_03_hatch1.zip>

 

우리가 원하는 것은 최소한 오른쪽 장면입니다. 이렇게 되기 위해서는 그림이 3장이 필요합니다. 그런데 가장 밝은 것을 흰색 = (1.0,1.0,1.0,1.0)으로 한다면 그림 추가 없이 쉐이더 코드 만으로도 해결이 가능합니다.

그림 추가는 해결이 되었다고 보고 이제는 3장의 이미지에 대한 레벨을 만들어야 합니다.

간단하게 밝기의 레벨을 3, 2, 1 로 정합시다. 그리고 흑백으로 만든 fW값에 3.5를 곱하고 이 값을 비교해 가면서 다음과 같이 각 레벨마다 이웃한 레벨끼리 선형 보간을 합니다.

 

float m_Level0 = 3.0f;

float m_Level1 = 2.0f;

float m_Level2 = 1.0f;

float4 White = {1,1,1,1};

 

float  Bright = t0.r * 0.299f + t0.g * 0.587f + t0.b * 0.114f;

 

 Bright *= 3.5f;

 

if( Bright>=m_Level0)

        Out = White;

 

else if( Bright>m_Level1)

        Out = (Bright - 2) * White + (3 - Bright)*t1;

 

else if( Bright>m_Level2)

        Out = (Bright - 1) * t1 + (2 - Bright)*t2;

 

else

        Out = t2;

 

이것을 실행하면 앞의 오른쪽 그림과 같은 화면을 얻을 수 있습니다. 그런데 위의 코드는 레벨의 값이 3, 2, 1 같은 간격으로 설정되어 있습니다. 만약 같은 간격이 아니라면 if~ else 구문이 다음과 같이 수정되어야 합니다.

 

else if( Bright>m_Level1)

{

        float   fW = 1- (m_Level0 - Bright)/(m_Level0-m_Level1);

 

        Out = fW * White + (1-fW)*t1;

}

 

else if( Bright>m_Level2)

{

        float   fW = 1- (m_Level1 - Bright)/(m_Level1-m_Level2);

        Out = fW * t1 + (1-fW)*t2;

}

 

이렇게 수정이 되고 레벨도 임의로 정해서 잘 동작하면 <연필선 이미지 C> 가 있는 그림을 레벨로 추가해서 앞의 코드처럼 다음과 같이 쉐이더를 만들 수 있습니다.

 

static float m_Level0 = 3.4f;

static float m_Level1 = 2.4f;

static float m_Level2 = 1.2f;

static float m_Level3 = 0.2f;

 

sampler smp0 : register(s0);

sampler smp1 : register(s1);

sampler smp2 : register(s2);

sampler smp3 : register(s3);

 

float4 PxlProc0(float2 uv : TEXCOORD0) : COLOR0

{

        float4 Out =0;                        // 출력 값

        static const float4 WHITE = {1,1,1,1};       // 흰색 이미지 대신

 

        float2  tx=  uv * 8.f;

        float4 t0 = tex2D(smp0, uv);

        float4 t1 = tex2D(smp1, tx);

        float4 t2 = tex2D(smp2, tx);

        float4 t3 = tex2D(smp3, tx);

 

        float  fW = 0;

        float  Bright = t0.r * 0.299f + t0.g * 0.587f + t0.b * 0.114f;

 

        Bright *=5;

 

        if(Bright > m_Level0)

               Out = WHITE;

 

        else if(Bright > m_Level1)

        {

               fW = (m_Level0 - Bright)/(m_Level0 - m_Level1);

               Out = (1-fW) * WHITE + fW * t1;

        }

        else if(Bright > m_Level2)

        {

               fW = (m_Level1 - Bright)/(m_Level1 - m_Level2);

               Out = (1-fW) * t1 + fW*t2;

        }

        else if (Bright > m_Level3)

        {

               fW = (m_Level2 - Bright)/(m_Level2 - m_Level3);

               Out = (1-fW) * t2 + fW*t3;

        }

        else

               Out = t3;

 

        Out = pow(Out, 2.f);

        Out *= 1.4f;

        return Out;

}

 

장면이 어두워 쉐이더 코드에서 pow() 함수를 사용해 명암 대비(Contrast)를 높였습니다. 추가된 그림 이미지로 인해 샘플러 register s3 사용을 지정했습니다. 프로그램에서는 다음과 같이  텍스처를 연결해야 하는 것은 당연합니다.

 

m_pDev->SetTexture(3, m_pTx3);

 

 

<연필 선 효과: h4_03_hatch3.zip, h4_03_hatch3_a.zip>

 

 

4.4 Distortion (Pencil Stroke)

4.4.1 화염 효과(Flame Effect)

Call of Duty - Modern Warfare 같은 게임을 하다 보면 전차의 엔진 뒤의 열기에 의한 아지랑이효과를 볼 수 있습니다. 또한 폭발 장면에서도 충격파가 등장하고 화염 주변에서도 사실감을 전달하기 위해 파티클(Particle)과 함께 화염 효과를 화면의 왜곡으로 표현된 것을 볼 수 있습니다.

이런 멋진 장면은 게임 프로그래머가 꼭 도전해 보고 싶은 과제이기도 합니다.

 

이런 충격파, 열기 등 대기 효과들을 화면에 렌더링 하기 위한 구조는 다음 그림과 각 단계로 단순화 시킬 수 있습니다.

 

<Distortion: 화염 효과 2D>

 

그림을 통해서 보면 전체 장면은 3부분으로 나누어 렌더링 합니다. 먼저 1 번 단계로 보통 3D를 렌더링 할 때와 마찬가지로 파티클과 장면을 그립니다. 이것을 실시간 텍스처에 저장합니다.

다음으로 화면을 왜곡시킬 파티클을 Distortion 맵으로 사용할 텍스처에 렌더링 합니다. 이 때의 렌더링은 변위 정보가 저장 되야 합니다. 변위정보의 저장은 파티클에 노이즈 이미지 등을 매핑 하고 렌더링 하면 됩니다.

세 번째 단계에서는 3D 장면을 저장한 텍스처와 2번 단계에서 파티클의 변위를 저장한 Distortion맵을 가지고 혼합합니다. 혼합 방법은 화면 잡음 때와 비슷하게 Distortion 맵의 정보를 가지고 3D 장면을 저장한 픽셀을 상하 또는 좌우로 움직이게 합니다.

 

방법에 수학적 물리적 이론이 있는 것은 아니니까 여러분은 각 단계 별로 하나씩 해결해 가면 구현의 어려움은 없을 것입니다.

먼저 1단계의 파티클을 화면에 출력해 봅시다. 이 파티클은 h4_04_distort0_particle.zip에 구현되어 있습니다. class CMcParticle 안에 하나의 파티클에 대한 동작을 제어할 구조체가 다음과 같이 선언되어 있습니다.

 

struct Tpart

{

D3DXVECTOR2    m_IntP;        // 초기 위치

        D3DXCOLOR      m_dCol;        // 색상

};

 

이 구조체를 가지고 만든 파티클은 CMcParticle::SetPart(int nIdx) 함수 안에 구현되어 있습니다

이 구조체와 초기함수로 파티클의 움직임은 CMcParticle::FrameMove() 안에서 재 설정 됩니다.

 

INT CMcParticle::FrameMove()

        for(i=0; i<m_PrtN; ++i)

        {

               CMcParticle::Tpart* pPrt = &m_PrtD[i];

 

               // 현재 위치 갱신

               pPrt->m_CrnP += pPrt->m_CrnV * ftime;

               float   f = (100 - rand()%201) * 0.01f;

               pPrt->m_CrnP.x +=f;

 

               // 경계 값 설정

               if(pPrt->m_CrnP.y<0.f|| pPrt->m_dCol.a < 0)

                       SetPart(i);

        }

 

        // 입자의 운동을 Vertex Buffer에 연결

        CMcParticle::VtxDRHW*  pVtx;

        m_pVB->Lock(0,0,(void**)&pVtx, 0);

 

        for(i=0; i<m_PrtN; ++i)

        {

               CMcParticle::Tpart* pPrt = &m_PrtD[i];

        }

 

        m_pVB->Unlock();

 

화염이 좌우로 흔들리기 위해 Random 함수로 f = (100 - rand()%201) * 0.01f; 값을 만들어서 파티클의 위치에 더했습니다. 파티클을 Point Sprite로 렌더링 하기 위해서 CMcParticle 객체를 만들 때 생성한 정점 버퍼에 위치와 색상을 복사를 합니다.

파티클은 CMcParticle::Render() 함수에서 렌더링을 합니다.

 

void CMcParticle::Render()

{

        m_pDev->SetRenderState(D3DRS_POINTSPRITEENABLE, TRUE);

        m_pDev->SetRenderState(D3DRS_POINTSCALEENABLE, TRUE);

        m_pDev->SetRenderState(D3DRS_POINTSIZE, FtoDW(120.f));

        m_pDev->SetRenderState(D3DRS_POINTSIZE_MIN, FtoDW(1.0f));

        m_pDev->SetRenderState(D3DRS_POINTSCALE_A, FtoDW(1.0f));

        m_pDev->SetRenderState(D3DRS_POINTSCALE_B, FtoDW(2.0f));

        m_pDev->SetRenderState(D3DRS_POINTSCALE_C, FtoDW(3.0f));

        if(::GetAsyncKeyState('R') & 0X8000)

        {

               m_pDev->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);

               m_pDev->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_DESTALPHA);

               m_pDev->SetTexture(0, m_pTx);

               m_pDev->SetStreamSource(0, m_pVB, 0, sizeof(CMcParticle::VtxDRHW));

               m_pDev->DrawPrimitive(D3DPT_POINTLIST, 0, m_PrtN);

        }

        else

        {

                m_pDev->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);

               m_pDev->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);

               m_pDev->SetTexture(0, m_pTx);

               m_pDev->SetStreamSource(0, m_pVB, 0, sizeof(CMcParticle::VtxDRHW));

               m_pDev->DrawPrimitive(D3DPT_POINTLIST, 0, m_PrtN);

        }

}

 

이 코드를 실행하면 다음 그림의 왼쪽의 화면이 보이며 이 화면을 Distortion 맵으로 만들 것입니다. 'R' 키를 누르면 오른 쪽 그림과 같이 보통 파티클을 렌더링 하는 장면이 나옵니다. 두 장면의 차이는 알파 블렌딩 옵션에서 D3DRS_DESTBLEND 에 대한 설정 값이 왼쪽그림은 INVSRCALPHA 이고 오른쪽 그림은 DESTALPHA 입니다. 이 점을 꼭 기억하기 바랍니다.

 

 

<파티클 효과: h4_04_distort0_particle.zip>

 

파티클을 만들었으니 이 파티클을 텍스처에 저장하는 2단계가 남았습니다. 클래스 CTexDistort는 변위 텍스처를 가진 파티클을 화면에 저장하기 위해서 파티클 객체와 IrenderTarget 객체를 가지고 있습니다. IrenderTarget은 장면을 텍스처에 저장하기 위한 객체 입니다.

 

class CTexDistort

{

        IrenderTarget* m_pTrnd;       // Rendering Target Texture for Scene

        CMcParticle*   m_pPrt;        // Particle Pointer

};

 

CTexDistort::FrameMove() 함수는 파티클의 움직임에 대한 정보를 먼저 갱신합니다. 다음으로 이 파티클을 IrenderTarget에 렌더링 합니다. 눈 여겨 볼 것은 색상버퍼를 Clear() 함수로 리셋 할 때 그 값을 D3DXCOLOR(0.5F, 0.5F, 0, 1) 값으로 하고 있습니다. 이 값은 쉐이더 코드에서 x y에 대해서 -0.5만큼 이동할 값입니다. , 색상 정보를 변위로 사용하고 싶은데 색상 값은 항상 양수이므로 쉐이더에서 그 차감만큼 기존 색상에 더해 주어야 하기 때문입니다. 화면을 Clear 하는 것은 또한 특정한 색상으로 채우는 것입니다.

앞서 강조한 알파 블렌딩 상태 D3DRS_DESTBLEND 에 대한 값을 INVSRCALPHA으로 설정함을 주의해야 합니다. 이 것을 바꾸면 변위의 위치가 파티클의 위치와 차이가 생겨 쉐이더 코드에서 더 내용을 추가 해야 합니다.

 

INT CTexDistort::FrameMove()

        // 파티클 갱신

        m_pPrt->SetTimeEps(g_pApp->m_fElapsedTime);

        m_pPrt->FrameMove();

 

        // 파티클을 렌더링 텍스처에 그린다.

        m_pTrnd->BeginScene();

 

        m_pDev->Clear(…, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, D3DXCOLOR(.5F,.5F,0,1),…);

 

        m_pDev->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE);

        m_pDev->SetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_DIFFUSE);

        m_pDev->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1);

 

        m_pDev->SetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_TEXTURE);

        m_pDev->SetTextureStageState(0, D3DTSS_ALPHAARG2, D3DTA_DIFFUSE);

        m_pDev->SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_MODULATE);

        m_pDev->SetRenderState(D3DRS_ALPHABLENDENABLE, TRUE);

        m_pDev->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);

m_pDev->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);

 

        m_pPrt->Render();

        m_pTrnd->EndScene();

 

이렇게 파티클을 갱신하고 Distortion 맵의 상태를 확인할 필요가 있습니다. 또한 CTexDistort 클래스는 파티클의 화염 효과도 렌더링을 담당하고 있으므로 이 두 가지 일을 동시에 처리하도록 하는 것도 좋습니다. CTexDistort::Render() 함수는 Distortion 맵을 확인하기 위해 이 맵을 화면에 출력하는 것과 장면에서 사용되는 화염 효과에 대한 렌더링 두 가지를 같이 하고 있습니다. Distortion 맵이 확인이 되면 이 부분은 나중에 주석으로 막아야 하는 코드 입니다. 다음은 Distortion 맵을 화면에 출력하는 CTexDistort::Render() 함수의 일 부분입니다.

 

struct Tvtx

{

        FLOAT   p[4];

        FLOAT   u,v;

} pVtx[4] =

{

        {   0,   0010, 0},

        { 200,   0011, 0},

        { 200, 150011, 1},

        {   0, 150010, 1},

};

 

PDTX    pTex = (PDTX)m_pTrnd->GetTexture();

 

// Distoriton 맵을 화면에 출력한다.

m_pDev->SetTexture(0, pTex);

m_pDev->SetFVF(D3DFVF_XYZRHW|D3DFVF_TEX1);

m_pDev->DrawPrimitiveUP( D3DPT_TRIANGLEFAN, 2, pVtx, sizeof(Tvtx));

 

다음은 CTexDistort::Render() 함수에서 화염 효과를 출력하는 부분입니다.

 

m_pDev->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE);

m_pDev->SetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_DIFFUSE);

m_pDev->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_MODULATE);

m_pDev->SetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_TEXTURE);

m_pDev->SetTextureStageState(0, D3DTSS_ALPHAARG2, D3DTA_DIFFUSE);

m_pDev->SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_MODULATE);

m_pDev->SetRenderState(D3DRS_ZENABLE, FALSE);

m_pDev->SetRenderState(D3DRS_ZWRITEENABLE, FALSE);

m_pDev->SetRenderState(D3DRS_LIGHTING, FALSE);

m_pDev->SetRenderState(D3DRS_ALPHATESTENABLE, FALSE);

m_pDev->SetRenderState(D3DRS_ALPHABLENDENABLE, TRUE);

m_pDev->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);

m_pDev->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_DESTALPHA);

m_pPrt->Render();

 

지금은 2차원 화면 공간에 RHW로 파티클이 마지막에 그려진다는 전제에 무조건 이전 색상을 덮어쓰기 위해서 ZENABLE FALSE로 했습니다. 이것을 3D에서 FALSE로 하면 모든 오브젝트를 통과해선 타납니다. 3차원 공간에서는 ZWRITEENALBLE FALSE하는 것이 더 좋습니다. 자세한 내용은 3D 프로그램 기초 고정 함수 파이프라인 강좌에 있는 알파 또는 깊이 테스트 부분을 참고 하기 바랍니다.

화염의 색상이 계속 밝아지기 위해서 DESTBLEND, 설정을 D3DBLEND_DESTALPHA으로 했습니다. 만약 DEST의 알파가 없으면 DEST ONE으로 설정하는 것과 동일합니다.

 

CTexDistort 클래스는 Distortion 텍스처를 쉐이더에서 사용할 수 있도록 텍스처를 가져올 수 있게 다음과 같은 함수도 추가하고 있어야 합니다.

 

PDTX CTexDistort::GetTexture()

{

        return (PDTX)m_pTrnd->GetTexture();

}

 

1, 2 단계의 과정이 다 끝났고, 마지막 단계인 Distortion 맵과 3D 장면을 합치는 3 단계가 남았습니다. 이를 위해 앞에서 계속 쉐이더를 시험하기 위해 사용했던 CShaderEx 클래스가 이 3단계를 처리 할 것입니다.

CShaderEx 클래스는 다음과 같이 CTexDistort 객체를 가지고 있습니다.

 

CTexDistort*   m_pDst;

 

CShaderEx 클래스의 Render() 함수는 3 단계를 다음과 같이 처리하고 있습니다. 쉐이더에서 sampler 레지스터를 지정해서 사용하고 있어서 각 다중 텍스처 단계에 대한 주소 모드, 필터링, 등을 전부 설정해야 합니다. 또한 텍스처를 2개를 파이프라인에 걸어주고 있는데 좌표는 1쌍 밖에 없으므로 1 번째 단계의 텍스처를 샘플링 할 때 0번째 텍스처 좌표를 가지고 하도록 다중 텍스처 단계 1 단계를 pDevice->SetTextureStageState(1, D3DTSS_TEXCOORDINDEX, 0)으로 해야 합니다. 렌더링이 끝나고 코드 마지막에 pDevice->SetTextureStageState(1, D3DTSS_TEXCOORDINDEX, 1) 로 다시 설정해야 하는 것은 당연합니다.

 

void CShaderEx::Render()

m_pDev->SetSamplerState(1, D3DSAMP_ADDRESSU, D3DTADDRESS_CLAMP);

m_pDev->SetSamplerState(1, D3DSAMP_ADDRESSV, D3DTADDRESS_CLAMP);

m_pDev->SetSamplerState(1, D3DSAMP_MINFILTER, D3DTEXF_LINEAR);

m_pDev->SetSamplerState(1, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR);

m_pDev->SetSamplerState(1, D3DSAMP_MIPFILTER, D3DTEXF_NONE);

m_pDev->SetTextureStageState(1, D3DTSS_TEXCOORDINDEX, 0);

 

FLOAT   hHeatHaze = .1f;

PDTX    pTex0 = m_pTex;

PDTX    pTex1 = m_pDst->GetTexture();

 

m_pEft->SetTechnique("Tech");

m_pEft->SetFloat("g_HeatHaze", hHeatHaze);

m_pEft->Begin(NULL, 0);

m_pEft->Pass(0);

 

m_pDev->SetTexture(0, pTex0);

m_pDev->SetTexture(1, pTex1);

m_pDev->SetFVF(VtxDUV1::FVF);

m_pDev->DrawPrimitiveUP( D3DPT_TRIANGLEFAN, 2, m_pVtx, sizeof(VtxDUV1));

m_pEft->End();

 

m_pDev->SetTextureStageState(1, D3DTSS_TEXCOORDINDEX, 1);

m_pDev->SetTexture(0, NULL);

m_pDev->SetTexture(1, NULL);

 

실행 파일 코드는 다 끝났습니다. 이제 쉐이더 코드가 남아 있습니다. 다음 쉐이더 코드를 보면 이전의 화면 잡음 효과 때와 거의 간단한 쉐이더 코드로 만들어져 있음을 볼 수 있습니다.

 

sampler smp0:register(s0);

sampler smp1:register(s1);

 

SvsOut VtxProc( float3 Pos:POSITION, float2 Tex:TEXCOORD0)

{

        SvsOut Out;

        Out.Pos = float4(Pos,1);

        Out.Tex = Tex;

        return Out;

}

 

float   g_HeatHaze = 0.05;

float4 PxlProc(SvsOut In) : COLOR0

{

        float4 Out=0;

        float4  Pert = tex2D(smp1, In.Tex);

        float   x = In.Tex.x + (Pert.x-0.5) * g_HeatHaze;

        float   y = In.Tex.y + (Pert.y-0.5) * g_HeatHaze;

 

        Out = tex2D(smp0, float2(x,y));

        return Out;

}

 

쉐이더 코드의 Pert.x-0.5, Pert.y-0.5 부분 때문에 앞서 Distortion 맵에 파티클을 렌더링 하기 전에 색상 버퍼를 D3DXCOLOR(0.5F, 0.5F, 1, 1)으로 Clear 했습니다.

쉐이더 코드의 픽셀 처리 함수 PxlProc(SvsOut In)를 보면 1 번째 텍스처의 색상 값을 가져와 0.5씩 빼주고  이 값에 HeatHaze 변수를 곱했습니다. 그리고 다시 원래의 UV좌표에 더했습니다. (Pert.x-0.5) * g_HeatHaze 부분이 결국 화면을 왜곡 시키는 요소가 되는 것입니다.

 

< Distortion 화염 효과: h4_04_distort2.zip>

 

이번에는 이것을 지형이 있는 3D 장면에 만들어 보겠습니다. 먼저 코드를 만들기 전에 여러분은 다음과 같은 그림을 생각하고 있어야 합니다.

 

<Distortion: 화염 효과 3D>

 

3D는 그림을 잘 그리면 코드건 수학이건 잘 풀립니다. 따라서 위와 같은 그림을 연습장에 그려놓고 코드 작업을 하는 것이 좋습니다.

 

이전과 거의 같은데 신경을 써야 하는 부분은 아무래도 Distortion 맵을 만드는 부분입니다. 단순히 파티클만 가지고 이 맵을 만들면 렌더링 오브젝트의 유무에 상관없이 무조건 화면이 흔들릴 것입니다. 따라서 오브젝트 앞에 있으면 효과를 주고 뒤에 있으면 안 주어야 합니다.

 

간단한 해결 방법은 변위를 만드는 파티클 이외의 나머지 다른 객체들은 이전에 화면을 Clear 할 때 사용했던 값 D3DXCOLOR(0.5F, 0.5F, 1, 1)으로 적용해서 그리면 파티클이 이 오브젝트 뒤에 있으면 Distortion이 적용이 안될 것입니다. 이를 위해 다음과 같은 쉐이더 코드가 필요합니다.

 

float4x4       m_mtWld;               // World Matrix;

 

SvsOut VtxPrcObj( float3 Pos : POSITION, float4 Dif : COLOR0, float2 Tex : TEXCOORD0)

{

        SvsOut Out=(SvsOut)0;

        Out.Pos = mul(float4(Pos,1), m_mtWld);

        Out.Tex = Tex;

        return Out;

}

 

앞의 정점 처리 쉐이더 코드는 정점의 위치를 변화 시키고 있고 텍스처 좌표를 픽셀 처리과정으로 넘기고 있습니다. 텍스처 좌표가 필요한 것은 텍스처의 알파 블렌딩 때문입니다. 텍스처의 알파블렌딩이 꺼져 있으면 나뭇잎을 사각형으로 그리게 되어 나무들 사이로 Distortion 효과가 제대로 반영되지 않게 되므로 꼭 텍스처 좌표도 픽셀 처리과정으로 넘겨야 됩니다.

 

float4 PxlPrcObj(SvsOut In) : COLOR0

{

        float4  Out={1,1,1,1};

 

        Out = tex2D(smp0, In.Tex);

        Out.r=0.5f; Out.g=0.5f; Out.b=0.5f;

        return Out;

}

 

출력 색상을 tex2D() 함수로 샘플링 하고 있습니다. 이 것은 출력 색상의 알파 값을 사용하기 위해서 입니다. 알파를 사용하기 위해서는 이 오브젝트를 그릴 때 다음과 같이 당연히 알파 블렌딩을 설정해야 합니다.

 

pDevice->SetRenderState( D3DRS_ALPHABLENDENABLE, FALSE);

pDevice->SetRenderState( D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);

pDevice->SetRenderState( D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);

 

다음으로 R, G, B0.5로 설정하고 있습니다. Blue Distortion 처리에서 사용하지 않으므로 어떤 값으로 설정해도 상관이 없습니다. 이렇게 해서 Distortion 맵을 만드는 쉐이더 코드는 끝이 났습니다.

 

이것을 3D 장면에 적용하고 싶은데 이전의 멈춰 있는 화면과 지금의 상황은 많이 다르다는 것을 눈치 챘을 것입니다. 달라진 부분의 첫 번째는 파티클 입니다. 이전 파티클은 화면 기준으로 만들어 졌는데 지금 파티클은 3D 공간에서 만들어야 합니다. 여기서는 3D 장면의 파티클 설명은 안 하겠습니다. 일단 파티클이 있다고 가정하고 그것이 이전과 같은 구조로 구성되어 있다면 그냥 같은 방식으로 이용해도 될 것입니다. 추가해야 할 것은 파티클을 3D 장면에 맞게 쉐이더로 다음과 같은 코드로의 수정입니다.

 

SvsOut VtxPrcPtc( float3 Pos : POSITION, float4 Dif : COLOR0, float2 Tex : TEXCOORD0)

{

        SvsOut Out=(SvsOut)0;

        float4  Tpos= mul(float4(Pos,1), m_mtWld);

        Out.Dif = Dif; Out.Tex = Tex;

 

        Tpos.x *=1.5f; Tpos.y *=1.5f;

 

        Out.Pos = Tpos;

        return Out;

}

 

변환이 끝난 후에 마지막에 1.5씩 곱했습니다. 이것은 파티클 크기에만 변위가 적용되면 화면에서는 파티클의 크기가 작으므로 우리가 원하는 효과가 잘 안 나타날 수 있어 좀 더 영역을 키운 것입니다.( 변환이 끝나면 [-1, 1]의 범위가 되므로 1.5란 값은 카메라가 파티클에 가까이 있을 때는 변화가 크게 작용합니다.)

 

변위를 만드는 파티클의 픽셀 쉐이더는 변위 텍스처를 샘플링 한 다음에 파티클 색상의 알파 값을 마지막에 곱해서 사용합니다. 이것은 파티클이 사라지면 변위가 나타나지 않게 하기 위해서 입니다. 또한 이런 코드가 제대로 작동하려면 앞서 알파 블렌딩 설정이 Source에는 SrcAlpha Dest InvSrcAlpha가 설정 되어야 합니다. (이 부분은 파티클 내부에서 처리하는 것이 좋습니다.)

 

float4 PxlPrcPtc(SvsOut In) : COLOR0

{

        float4  Out={1,1,1,1};

        float4  t1= In.Dif;

 

        Out = tex2D(smp0, In.Tex);

        Out.w *= t1.w;

        return Out;

}

 

파티클은 장면을 만들어야 하는 객체이므로 CTexDistort 클래스에서 가져야 할 이유가 없습니다. 그리고 파티클을 통해서 Distortion 맵이 만들어지는 것이라서 파티클 객체가 선언되어 있는 장소에 IrenderTarget 객체를 같이 있는 것이 좋습니다. 이런 이유로 CTexDistort 클래스는 필요 없어지고 나무, 지형이 모여 있는 CMain으로 파티클 객체와 IrenderTarget 객체를 옮겨야 됩니다.

 

class CMain

IrenderTarget* m_pTrndS       ;              // for scene

IrenderTarget* m_pTrndD       ;              // for distortion

 

CMcField*      m_pField       ;

CMcParticle*   m_pPrt         ;

 

두 번째 달라진 부분은 전체 장면을 텍스처에 Distortion 맵과 장면 텍스처에 두 번 렌더링이 되도록 작성해야 한다는 것입니다. CMain::RenderScene() 함수는 이렇게 2부분으로 거칠게 다음과 같이 코딩하고 있습니다.

 

void CMain::RenderScene(BOOL bDistortion)

        if(FALSE == bDistortion)

        {

               m_SkyBox->Render();

               …

               m_pField->Render();

               …

               for(int i=0; i<m_TreeNum; ++i)

               {

                       m_pd3dDevice->SetTransform(D3DTS_WORLD, &m_TreeMat[i]);

                       m_TreeMsh->Render();

               }

               …

               m_pPrt->Render(TRUE);

        }

        else {

               …

               mtVP    = mtViw * mtPrj;

 

               hr = m_pEft->SetTechnique("Tech");

               hr = m_pEft->Begin(NULL, 0);

               hr = m_pEft->Pass(1);

 

               mtWld   = mtViw * mtPrj;

               hr = m_pEft->SetMatrix("m_mtWld", &mtWld);

               m_pField->Render();

               …

               for(int i=0; i<m_TreeNum; ++i)

               {

                       mtWld = m_TreeMat[i] * mtVP;

                       hr = m_pEft->SetMatrix("m_mtWld", &mtWld);

                       m_TreeMsh->Render();

               }

               …

               hr = m_pEft->Pass(2);

               mtWld   = mtVP;

               hr = m_pEft->SetMatrix("m_mtWld", &mtWld);

               m_pPrt->Render(FALSE);

               hr = m_pEft->End();

 

ID3DXEffect에 대한 Pass 도 상당히 많아졌습니다.

 

technique Tech

{

        pass P0        // Distortion Map 3D 장면 텍스처 합성

        {

               VertexShader = compile vs_1_1 VtxPrcDst();

               PixelShader  = compile ps_2_0 PxlPrcDst();

        }

 

        pass P1        // Distortion Map에 대한 오브젝트 렌더링

        {

               VertexShader = compile vs_1_1 VtxPrcObj();

               PixelShader  = compile ps_2_0 PxlPrcObj();

        }

 

        pass P2        // Distortion Map에 대한 파티클 렌더링

        {

               VertexShader = compile vs_1_1 VtxPrcPtc();

               PixelShader  = compile ps_2_0 PxlPrcPtc();

        }

};

 

같은 파일에 있어 CShaderEx CMain은 같은 ID3DXEffect 객체를 사용해도 되나 관리의 편의상 각각 따로 ID3DXEffect 객체를 가지고 있도록 했습니다.

 

CShaderEx 클래스는 Distortion Map의 텍스처 포인터를 설정할 수 있도록 SetDistortionTexture() 함수가 추가되었습니다.

또한 CShaderEx::Render() 함수에서 쉐이더 설정에서 디바이스의 1번 샘플러에게 Distortion 텍스처를 설정하고 있음을 볼 수 있는데 이것이 가능 하려면 쉐이더 코드에서 샘플러를 레지스터에 "sampler '샘플러 객체 이름' : register(s1)"처럼 지정해야 합니다.

 

m_pEft->SetTechnique("Tech");

….

m_pDev->SetTexture(1, pTex1);

m_pDev->DrawPrimitiveUP( D3DPT_TRIANGLEFAN, 2, m_pVtx, sizeof(VtxDUV1));

 

이렇게 쉐이더 코드와 렌더링 코드를 무사히 마쳤다면 다음과 같은 장면과 같이 화염의 앞에 있는 오브젝트에 대해서는 왜곡이 안 발생하고 불꽃 뒤에 있는 오브젝트에 대해서만 왜곡이 만들어지는 것을 볼 수 있습니다.

 

< Distortion 3D 장면의 화염 효과: h4_04_distort2_a_1.zip>

 

ShaderEx.cpp 파일의 #define MYSHADER_APP 1 설정을 MYSHADER_APP 0 으로 하면 Distortion 맵이 화면 사이즈보다 축소시켜서 렌더링 하고 있는 것을 볼 수 있습니다.

 

 

4.4.2 굴절 효과(Refraction Effect)

보통 반사나 굴절을 3D에서 표현하려면 환경 맵(Environment Map), 또는 입방체 맵(Cube Map)을 만들어야 합니다. 하지만 앞의 화염 효과를 잘 생각해보면 파티클 대신 어떤 오브젝트에 의해 변위를 만들 수 있지 않을 까요?

그러면 오브젝트의 무엇으로 변위를 만들 수 있을 까요?

이것도 생각해보면 굴절이라는 것은 투명한 물체의 두께에 의해 발생하는 문제입니다. 그런데 속이 비어있는 3D 렌더링 오브젝트의 두께를 카메라의 시선 방향으로 구하고 굴절을 적용한다는 것은 쉬운 일은 아닙니다.

우리가 만드는 프로그램은 실 세계의 흉내내기 입니다. 따라서 적당한 방법을 찾아야 하는데 간단한 방법은 오브젝트의 법선 만큼 픽셀을 이동시키는 것입니다. 이것을 마무리된 코드의 장면을 가지고 그림으로 표현하면 다음과 같습니다.

 

< Distortion: 3D 장면의 굴절>

 

앞의 화염 효과 코드에서 2 번의 과정과 이에 관련된 쉐이더 코드를 수정하는 일만 남아있습니다.

먼저 쉐이더 코드입니다.

 

법선은 픽셀 처리과정에서 사용해야 하므로 쉐이더의 정점 구조체를 다음과 같이 수정해야 합니다.

 

struct SvsOut

{

        float4 Pos : POSITION;

        float4 Dif : COLOR0;

        float2 Tex : TEXCOORD0;

        float3 Nor : TEXCOORD7;

};

 

카메라의 움직임에 의해서 굴절에 대한 오브젝트의 법선은 뷰 변환까지 진행이 되야 합니다.

이를 외부에서 연결할 수 있도록 월드 행렬 * 뷰 행렬 에 대한 변수를 추가합니다.

float4x4       m_mtWVP;       // World * View * Projection

float4x4       m_mtWV ;       // World * View

 

굴절 역할을 하는 오브젝트는 법선 벡터를 입력 레지스터에서 받을 수 있도록 float3 Nor:NORMAL0를 추가하고 법선이 카메라의 회전에만 적용되도록 float3x3으로 "월드 * 뷰 행렬"을 캐스팅해서 변환합니다.

 

SvsOut VtxPrcRfc( float3 Pos : POSITION

               , float4 Dif : COLOR0

               , float3 Nor : NORMAL0

               , float2 Tex : TEXCOORD0)

{

        SvsOut Out=(SvsOut)0;

        float3 N= normalize(mul(Nor, (float3x3)m_mtWV));

        Out.Pos = mul(float4(Pos,1), m_mtWVP);

        Out.Tex = Tex;

        Out.Nor = N;

        return Out;

}

 

픽셀 처리과정의 굴절 오브젝트는 텍스처에서 알파 값을, 정점 처리 과정에서 구한 법선벡터를 색상으로 저장합니다. 그런데 색상은 [0, 1] 범위이고 법선은 [-1, 1] 범위 이므로 법선에 1을 더하고 0.5를 곱해서 저장을 합니다. 출력 색상의 Blue값은 확인용으로 쓰기 위해서 임의 값을 부여했습니다.

 

float4 PxlPrcRfc(SvsOut In) : COLOR0

{

        float4  Out={1,1,1,1};

        float3  Nor= normalize(In.Nor);

 

        Out = tex2D(smp0, In.Tex);

 

        Out.r=(Nor.x+1)*0.5;

        Out.g=(Nor.y+1)*0.5;

        Out.b=0.5f;

        return Out;

}

이렇게 색상으로 법선 벡터를 변경해서 저장했습니다. Distortion을 처리하는 쉐이더 함수에서는 이것을 다시 환원하기 위해 굴절 정보를 저장한 텍스처(smp1)에서 색상을 2를 곱한 다음 1을 뺍니다. 이 값을 적당한 굴절 계수를 곱한 후에 다시 전체 장면(smp0)의 픽셀을 샘플링 합니다.

 

static float   g_Dsp = 0.3;

 

float4 PxlPrcDst(SvsOut In) : COLOR0

{

        float4 Out=0;

        float4  Pert = tex2D(smp1, In.Tex);

 

        float   x = Pert.x * 2 -1;

        float   y = Pert.y * 2 -1;

 

        x = In.Tex.x + (x) * g_Dsp;

        y = In.Tex.y + (y) * g_Dsp;

 

        Out = tex2D(smp0, float2(x,y));

        Out *= 1.5f;

        return Out;

}

 

앞의 VtxPrcRfc(), PxlPrcRfc() 두 함수는 물론 Technique 에 추가해야 합니다.

 

technique Tech

{

        pass P3

        {

               VertexShader = compile vs_1_1 VtxPrcRfc();

               PixelShader  = compile ps_2_0 PxlPrcRfc();

        }

};

 

이제 CMain::FrameMove()함수를 수정할 차례입니다. CMain 클래스에 굴절 효과를 표현할 오브젝트를 추가합니다.

 

ID3DXMesh*             m_pCrystal1    ;

 

이 객체를 D3DXCreateCylinder()함수로 생성합니다.

 

D3DXCreateCylinder(m_pd3dDevice, 70, 70, 200, 100, 100, &m_pCrystal1, NULL);

 

CMain::RenderScene() 함수에서 이전 파티클에 해당하는 부분을 지우고 굴절 오브젝트를 넣습니다.

 

D3DXMatrixScaling(&mtScl, 1, 1, 1);

D3DXMatrixRotationX(&mtRot, D3DXToRadian( 90 ));

D3DXMatrixTranslation(&mtTrn, 300, 40, 250);

 

mtWld = mtScl * mtRot * mtTrn;

mtWV  = mtWld * mtViw;

mtWVP = mtWV * mtPrj;

 

hr = m_pEft->Pass(3);

hr = m_pEft->SetMatrix("m_mtWVP", &mtWVP);

hr = m_pEft->SetMatrix("m_mtWV", &mtWV);

m_pCrystal1->DrawSubset(0);

 

CShaderEx::Render() 함수에서 전에 사용한 HeatHaze변수는 사용을 안 해 지워버립니다.

나머지 코드를 정리하고 실행하면 다음과 같은 화면을 얻을 수 있습니다.

 

 

<굴절 효과: h4_04_distort3_refr.zip. g_Dsp = -0.3, g_Dsp = 0.3>

 

그런데 이 예제는 문제가 있습니다. 다음 그림과 같이 카메라가 오브젝트에 가까이 가면 옆으로 픽셀이 늘어납니다. 이것은 변위를 적용한 U, V [0, 1] 범위를 벗어나기 때문입니다. 또한 오브젝트 안쪽으로 들어가면 굴절 효과가 거의 없어집니다. 이런 경우만 제외한다면 완전한 굴절 모양은 아니지만 적당히 사용할 만 합니다.

 

<굴절 효과 문제>

 

 

4.5 흐림 효과

4.5.1 흐림 효과(Blur Effect) 개요

흐림 효과는 broadening의 일종으로 주변의 픽셀과 혼합하는 방법입니다. Bloom, Glare 효과 모두

<3x3 박스 필터(g: 가중치)>

흐림 효과를 기본으로 만든 이펙트입니다. 흐림 효과는 수학으로 표현하면 적분이 됩니다. 참고로 수학의 미분 개념을 이용한 효과는 외곽선 추출, Sharpness가 됩니다.

그런데 얼마만큼의 주변 픽셀과 어떤 방법으로 섞는가? , 혼합에 대한 방법이 많이 있는데 가장 간단한 방법은 옆의 그림처럼 자신과 바로 인접해 있는 9개의 픽셀에 대한 가중치(Gravitation)를 전부 1로 놓고 이 픽셀들의 색상에 대한 평균 값을 최종 색상으로 정하는 방법입니다.

이 방법은 가장 간단해서 사용된 방법이지만 더 근본적으로 픽셀 쉐이더 2.0 미만에서 한 번에 샘플링 할 수 있는 텍스처의 수가 많아야 6개 정도이고 이것도 픽셀 쉐이더 버전 1.4를 지원하는 ATI 제품일 때만 가능한 것입니다. 더 하위버전인 1.3 까지는 최대 4개만 샘플링이 가능합니다. 따라서 낮은 버전에 맞추어 흐림 필터를 위와 같은 방식으로 만들었던 것입니다.

 

그런데 요즘은 대부분의 그래픽 카드가 픽셀 쉐이더 2.0이상을 지원합니다. 2.0의 경우 16개의 픽셀을 동시에 샘플링 할 수 있으며 이것은 하나의 텍스처에서도 16번의 샘플링이 가능하다는 것입니다. 이렇게 한 번에 한 텍스처에 16번의 픽셀을 가져와 사용할 수 있는데 앞서 가중치를 동일하게 주고 평균을 내는 것은 개선이 되어야 합니다.

 

, 가운데 있는 픽셀로부터 멀리 떨어질수록 거리에 따라 가중치를 다르게 설정해야 합니다. 이때 가우스 분포 함수(Gaussian Distribution Function)를 사용합니다.

<2차원 가우스 분포함수>

 이 함수를 사용하는 이유는 자연계에서 시간에 따라 확률이 변화하지 않는 경우에 대해서 그 확률에 맞게 어떤 일이 발생하는 일들을 그래프로 표현해 보면 이 가우스 분포 곡선과 일치하게 됩니다. 예를 들어 동전 100개를 가지고 동시에 던졌을 때 앞면이 100, 99, 98, …, 0개가 나오는 빈도를 그래프로 찍어보면 이 가우스 분포 함수와 거의 일치합니다.

가우스 함수는 일반 적으로 Random 한 데이터에서 나타나는 특성으로, 통계뿐만 아니라 물리, 화학, 사회과학 등에서 사건에 대한 해석을 하는 용도로 두루 사용이 되는 함수입니다.

 

따라서 컴퓨터 그래픽스에서도 이웃한 픽셀과 혼합할 때 그 가중치를 가우스 분포 함수의 값을 이용합니다.

 

<3차원 가우스 분포 함수>

 

가중치(Weight) = 가우스 분포함수 =

 

 : Intensity(상수),  : 중심 픽셀과 x 방향으로의 거리, : 중심 픽셀과 x 방향으로의 거리, : 분산 값

 

만약 중심 픽셀의 가중치를 1로 놓게 되면 Δx=0, Δy =0 이 되어  =1 이 되어야 합니다.

게임프로그래밍에서는 대부분 로 설정하고  만 조절할 수 있도록 다음과 같이 간략하게 만들어 사용합니다.

 

가중치 =

 

쉐이더 프로그램은 픽셀 샘플링의 제한이 있어 이렇게 X, Y 방향으로 동시에 처리하지 않고 한 방향씩 처리합니다. 따라서 가중치 계산은 1차원에서만 계산이 되며 다음은 가장 많이 사용이 되는 형태 입니다.

 

가중치(W) =

 

이 함수에 익숙해지기 위해 예를 들어 봅시다. 만약 =-0.08 일 때 거리에 따라 가중치를 구하면 다음과 같습니다.

 

=0: 02 = 0.  가중치 = exp( 0* (-0.08)) = 1

=1: 12 = 1.  가중치 = exp( 1* (-0.08)) = 0.9231

=2: 22 = 4.  가중치 = exp( 4* (-0.08)) = 0.7261

=3: 32 = 9.  가중치 = exp( 9* (-0.08)) = 0.4868

=4: 42 = 16. 가중치 = exp(16* (-0.08)) = 0.2780

=5: 52 = 25. 가중치 = exp(25* (-0.08)) = 0.1353

=6: 62 = 36. 가중치 = exp(36* (-0.08)) = 0.0561

=7: 72 = 49. 가중치 = exp(49* (-0.08)) = 0.0198

가중치의 합 = 6.2108

 

가중치의 합은 범위가 -7, -6, -5, …, 5, 6, 7 에 해당하는 가중치에 대한 총합 입니다.

가중치를 구했으면 최종 색상은 다음과 같이 결정이 됩니다.

 

최종 색상 =

 

이것을 쉐이더에 대한 의사(Pseudo-do) 코드로 작성하면 다음과 같습니다.

 

Weight[N] ß {"사용자 지정" }, Total

"최종 색상" ß 0

for(iß0; i<N; ißi+1)

{

        "새로운 UV" ß "중심 픽셀 UV" + float2( i * "픽셀 가로 폭", 0)

        "최종 색상" ß "최종 색상" + Weight[i] * tex2D(sampler0, "새로운 UV");

}

 

"최종 색상" ß "최종 색상"/Total;

 

여기서 텍스처 좌표는 [0, 1] 이므로 픽셀의 가로 폭 = 1/"텍스처의 가로 폭" 이 됩니다.

 

이것을 h4_05_blur2_1.zip "data/hlsl.fx" 파일에 픽셀을 처리하는 함수에 다음과 같이 구현되어 있습니다.

 

float   m_TexW = 800;

float   m_TexH = 600;

 

static const float Weight[13]=

{

   0.0561, 0.1353, 0.278, 0.4868, 0.7261, 0.9231,

1, 0.9231, 0.7261, 0.4868, 0.278, 0.1353, 0.0561};

 

static const float Total = 6.2108;

 

float4 PxlBlurX(SvsOut In) : COLOR0

{

        float4  Out=0;

 

        float2  t = In.Tex;

        float2  uv = 0;

        float   tu= 1./m_TexW;

 

        for(int i=-6; i<6; ++i)

        {

               uv = t+ float2(tu *i, 0);

               Out += Weight[6+i] * tex2D(smp0, uv);

        }

 

        Out /=Total;

        return Out;

}

 

h4_05_blur2_1.zip을 실행하면 다음 그림과 같이 오른쪽 화면에 흐림 효과를 적용한 것이 보일 것입니다. 흐림 효과를 저장하기 위해서 CShaderEx 클래스는 IrenderTarget 객체를 하나 가지고 있습니다.

 

<X 방향 흐림 효과: h4_05_blur2_1.zip>

 

X 방향 흐림 효과는 이전의 박스 흐림 필터와 눈에 띄게 큰 차이를 보이지 않습니다. 이 것을 한번 더 Y 방향으로 흐리게 처리해 봅시다. 쉐이더 코드는 X 방향으로 처리할 때와 거의 같으며 단지 텍스처의 높이와 Y 축으로 샘플링 되도록 u, v를 수정하는 것뿐입니다.

 

float4 PxlBlurY(SvsOut In) : COLOR0

        float   tv= 1./m_TexH;

 

        for(int i=-6; i<6; ++i)

        {

               uv = t+ float2(0, tv *i);

               Out += Weight[6+i] * tex2D(smp0, uv);

        }

 

이 함수 또한 Techinque에 추가 해야 합니다. 다음으로 Y 방향으로 처리한 결과를 저장하기 위해서 IrenderTarget 객체를 하나 더 추가하고 X 방향으로 흐림 효과를 처리한 텍스처를 Y 방향으로 다시 흐림 효과를 적용합니다. 이 과정은 h4_05_blur2.zipCShaderEx::FrameMove() 함수에 다음과 같이 구현되어 있습니다.

 

// 원 장면 텍스처

pTx= m_pTex;

 

// X 방향 흐림 효과를 저장

m_pTrndX->BeginScene();

hr = m_pDev->SetTexture(0, pTx);

hr = m_pEft->Pass(0);

m_pDev->DrawPrimitiveUP( D3DPT_TRIANGLEFAN, 2, m_pVtx, sizeof(VtxDUV1));

m_pTrndX->EndScene();

 

 

// Y 방향 흐림 효과를 저장

m_pTrndY->BeginScene();

pTx= (PDTX)m_pTrndX->GetTexture();    // X 방향 흐림 효과를 저장한 텍스처를 가져온다.

hr = m_pDev->SetTexture(0, pTx);

hr = m_pEft->Pass(1);

m_pDev->DrawPrimitiveUP( D3DPT_TRIANGLEFAN, 2, m_pVtx, sizeof(VtxDUV1));

m_pTrndY->EndScene();

 

다음 그림의 왼쪽 부분은 X 방향으로만 흐림 효과를 적용한 것이고 오른쪽 부분은 왼쪽에서 처리한 것을 Y 방향으로 다시 처리한 결과 입니다

 

<흐림 효과: h4_05_blur2_2.zip X, Y 양방향으로 적용>

 

만약 흐림 효과를 화면 전체에 적용한다고 했을 때 X, Y에 대해서 반복적으로 흐림 효과를 적용해야 할 것입니다. 다음은 X, Y 방향에 대해서 각각 8번씩 흐림 효과를 반복적으로 적용한 그림입니다.

 

<흐림 효과: h4_05_blur2_2.zip>

 

예상대로 반복한 만큼 많이 흐려졌습니다. 그러나 문제는 렌더링 속도 입니다. 이렇게 렌더링 속도를 깎아 먹으면 다른 효과는 시도도 못할 것입니다. 문제를 해결하기 위해서 흐림 효과라는 것을 되돌아 보면 주변 픽셀과 뭉개기 인데 이 결과를 저장하는 텍스처의 크기를 굳이 화면 크기와 같은 크기로 만들 필요가 없다는 것입니다.

그래서 Blur를 저장할 텍스처의 크기를 줄여서 출력해 보았습니다.

 

 

<흐림 효과. Blur 4회 반복. 텍스처 크기: 화면x (1/2) 4 화면x (1/4). h4_05_blur2_2.zip>

 

Blur 횟수를 절반으로 줄이고 텍스처의 크기는 1/2 이하로 줄여도 화면의 효과는 비슷합니다. 대신 렌더링 속도는 이전과 비교할 수 없을 정도 향상됩니다. , 픽셀을 뭉갤 때는 화면 크기보다 작은 텍스처를 가지고 작업하는 것입니다.

 

이렇게 테스트 코드를 만들었는데 3D 장면에 적용해 봅시다. 3D에 적용되는 과정은 다음 그림과 같을 것입니다.

 

<흐림 효과 적용 방법>

 

이전 효과들에서 사용했던 코드에 적용해 봅시다. CShaderEx 클래스는 흐림 효과를 FrameMove()함수에서 처리하고 있습니다. 따라서 CMain::FrameMove()함수에서 장면을 텍스처에 저장하고 나서 CShaderEx 객체에 장면에 대한 텍스처 포인터를 전달해야 합니다.

 

HRESULT CMain::FrameMove()

m_pTrnd->BeginScene();

hr = m_pd3dDevice->Clear(…);

RenderScene();

m_pTrnd->EndScene();

 

// CShaderEx클래스3D 장면 텍스처 연결

LPDIRECT3DTEXTURE9     pTx = (LPDIRECT3DTEXTURE9)m_pTrnd->GetTexture();

m_pShader->SetSceneTexture(pTx);

SAFE_FRMOV(    m_pShader      );

 

쉐이더 코드의 sampler에 대한 객체 설정에서 sampler smp0:register(s0); 대신 다음과 같이 수정했습니다.

 

texture m_TxDif;

sampler smpDif = sampler_state

{

        texture = <m_TxDif>;

};

 

따라서 디바이스의 SetTexture()함수가 아닌 이펙트 객체의 SetTexture() 함수를 사용해야 합니다.

 

m_pEft->SetTexture("m_TxDif", pTx);

 

모든 코드는 h4_05_blur2_a.zip에 있으며 실행하면 다음과 같은 장면을 만들어 냅니다.

 

 

<흐림 효과 적용: h4_05_blur2_a.zip>

 

 

4.5.2 Glare Effect

흐림 효과는 포스트 이펙트에서 아주 자주 사용되는 기술입니다. 이 방법은 조금만 수정하면 Glare 효과에 쉽게 적용할 수 있습니다. Glare 효과는 아주 밝은 빛 아래의 몽환(夢幻)적 표현이나 어두운 곳에서 갑자기 밝은 곳으로 나왔을 때 등 많은 부분에서 자주 사용됩니다.

 

<Glare 효과 적용 순서>

 

지금까지의 내용에 이 효과를 구현하려면 앞의 그림처럼 먼저 장면을 저장한 텍스처에서 밝은 부분을 추려냅니다. 밝은 부분을 추려내는 방법은 쉐이더의 pow() 함수를 이용합니다. 이 함수는 조명의 퐁 반사에서 설명했듯이 어떤 수에 승수를 해주는 함수입니다. 장면의 픽셀에 이 함수를 사용하면 밝은 부분을 얻어낼 수 있고, 이렇게 얻은 픽셀에 흐림 효과를 적용합니다. 마지막에는 전체 장면과 적절한 연산을 합니다.

2단계의 밝은 색 추출은 다음과 같이 pow() 함수를 이용합니다.

 

float4 Pxlsharp(SvsOut In) : COLOR0

{

        float4  Out= 0;

        float4 t0 = tex2D(smpDif, In.Tex);

        Out = pow(t0,4);

        Out *= 30;

        Out.w = 1;

        return Out;

}

 

다음은 4단계에서 장면 텍스처와 흐림 효과 텍스처를 섞는 쉐이더 코드 입니다.

 

float4 PxlMulti(SvsOut In) : COLOR0

{

        float4  Out=0;

        float4 t0 = tex2D(smpDif, In.Tex);

        float4 t1 = tex2D(smpDiB, In.Tex);

 

        //Out = t0 + t1*2.5f;

        Out = t0*.7 + t1*0.7f;

        Out *= 1.5f;

        Out.w = 1;

        return Out;

}

 

전체적으로 2단계가 더 추가되어 Technique도 수정해야 합니다. h4_05_blur3_a.zip 예제는 가우스 분포 exp ()함수를 직접 사용하고 있습니다. 이전 결과와 거의 차이가 없게 구성했습니다.

 

for(int i=-fBgn; i<=fBgn; ++i)

{

        uv = In.Tex + float2(i*fInc/m_TexW, 0);

        Out += tex2D(smpDif, uv) * exp( -i*i * fDelta);

}

 

전체 코드는 h4_05_blur3_a.zip을 참고 하기 바랍니다.

 

  

<Glare 효과: h4_05_blur3_a.zip. RGB, BRG, GBR교환>

 

만약 좀 더 밝게 만들고자 한다면 밝은 부분만 추려내는 쉐이더의 Pxlsharp()함수의 내용을 수정해야 합니다.

 

 

4.5.3 Cross Filter Effect

<DXSDK HDR 예제>

유리나 안경 등에서 강한 빛이 여러 개의 방향으로 분산 되는 현상을 가끔 볼 수 있는데 카메라에서는 이런 효과를 극대화 하기 위해 크로스 필터(Cross Filter) 라는 것을 사용합니다. DXSDKHDRLighting 예제는 HDR(High Dynamic Range)에 대한 장면을 연출하고 있습니다.

예제의 설명은 SDK 안에서도 자세히 설명하고 있으니 도움말을 참고 하기 바랍니다. 여기서는 이 예제에 사용된 크로스 필터만 만들어 보겠습니다.

크로스 필터를 적용하는 방법은 다음 그림과 같이 먼저 장면에서 아주 밝은 고휘도(High Brightness or Luminescence) 부분을 추출해야 하고 이것을 이미지에 저장을 해야 합니다.

만약 6방향으로 분산하는 빛을 표현하고 싶다면 6방향으로 늘려야 합니다. 6방향에 대한 결과를 저장해야 하므로 여기세 최소한 6장의 이미지와 이 결과를 저장할 한 장의 이미지 총 7장이 필요합니다.

 

<크로스 필터 만드는 방법>

 

쉐이더 코드로 6방향으로 색상을 늘리게 되면 군데군데 빛이 끊어질 수 있습니다. 이것을 메우기 위해 흐림 효과를 적용합니다. 이렇게 적용하고 나서 장면과 다시 합칩니다.

 

전체적인 내용은 이전의 흐림 효과에 밝은 부분 추출과 6방으로 늘려 처리하는 과정이 더 추가가 되었습니다. 이것은 그냥 하면 될 것 같기도 합니다. 그런데 여기서 하나 더 알아야 할 것이 있습니다. 그것은 밝은 부분을 더욱 강하게 저장해 놓아야 6방향으로 늘이면서 점점 빛의 세기를 줄여 가며 크로스 필터 효과를 만들어야 하는데 기존의 32비트 텍스처는 각 색상의 표현이 최대 255 밖에 안됩니다. 따라서 쉐이더 코드에서 밝기를 아무리 올려 놓아도 최대 255 이상은 소용이 없게 됩니다.

이런 이유로 각 색상을 8비트 보다 더 큰 정보를 저장할 수 있는 텍스처가 필요합니다. DXSDK는 아주 높은 휘도의 색상 정보를 저장할 수 있는 각 채널당 16비트 부동 소수점 형식의 D3DFMT_A16B16G16R16F 32비트 형식의 D3DFMT_A32B32G32R32F 포맷을 지원하고 있습니다. 이 둘 중의 하나로 텍스처를 만들어야 고휘도 처리 결과가 제대로 저장이 됩니다.

 

대충 어떤 형식으로 처리해야 하는지 방법을 알았으니까 본격적으로 구현해 봅시다. 먼저 각 단계별 텍스처들을 모아 보면 다음과 같이 전체 장면을 저장할 텍스처 1, 고휘도 추출을 위한 텍스처 1 , 6방향과 이를 저장할 텍스처 7장 이 필요한데 고휘도 추출을 먼저 Y 방향 흐림 효과에 사용할 텍스처에 저장하면 되므로 총 10장 정도의 텍스처가 필요합니다.

 

IrenderTarget* m_pTrnd;               // Rendering Target Texture for Scene

IrenderTarget* m_pTrndX;              // Rendering Target Texture for Blur X

IrenderTarget* m_pTrndY;              // Rendering Target Texture for Blur Y

IrenderTarget* m_pTrndC[6];           // Cross Texture

IrenderTarget* m_pTrndS;              // Cross Texture All

 

IrenderTarget 는 실시간 텍스처를 생성해 주는 클래스 입니다. 이 클래스에 대한 객체를 생성할 때 다음과 같이 "HDR16" 문자열을 넣어주면 채널당 16비트 D3DFMT_A16B16G16R16F 포맷의 텍스처를 생성해 줍니다. 내부 코드는 h4_06_cross1.zip RenderTarget.cpp 에 있으니 참고 하기 바랍니다.

 

m_fTxW = 400;

m_fTxH = 300;

LcD3D_CreateRenderTarget("HDR16", &m_pTrndX, m_pDev, m_fTxW, m_fTxH);

 

텍스처 생성은 이렇게 끝내고 다음으로 고휘도 추출하는 HLSL부분입니다.

 

float4 PxlLumi(SvsOut In) : COLOR0

        float4  Out=0;

        float4 t0 = tex2D(smpDif, In.Tex);

 

        t0.g *=1.11f; t0.b *=0.9f;

        t0 = pow(t0,4);

       

        if(t0.r+t0.b+t0.b<0.95f)

               t0=0.f;

 

        t0 = smoothstep(0.6,0.9, t0);

        t0 = pow(t0,12)*2;

        Out = t0;

 

전체 장면이 파란색이 많아 녹색을 조금(1.11f) 강조했습니다. 코드 중간에 pow() 함수를 사용해서 색상의 밝기가 1 근처에 있는 픽셀만 남겨놓도록 하고 있습니다.

smoothstep()함수는 세 인수, min, max, 색상을 받아서 색상이 min 보다 작거나 같으면 0, max보다 크거나 같으면 1을 반환 합니다. 그 사이에 있는 함수는 그림처럼 에르미트 보간(Hermite Interpolation)을 하는 함수입니다.

 

< Hermite Interpolation >

float smoothstep(float min, float max, float x)

{

x = saturate((x - min) / (max - min));

 

    return x*x*(3-2*x);

}

saturate: x<0 x=1, x>1 x=1

 

이 쉐이더 코드는 CShaderEx::FrameMove() 에서 2번째 고휘도 추출에서 호출됩니다.

 

INT CShaderEx::FrameMove()

// 2. 축소된 텍스처에 Luminescence가 강한 부분을 그린다.

m_pTrndY->BeginScene();

hr = m_pDev->SetVertexDeclaration(m_pFVF);

hr = m_pEft->Begin(NULL, 0);

        hr = m_pEft->Pass(0);

        hr = m_pDev->DrawPrimitiveUP();

 

6방향으로 늘리는 작업은 쉐이더코드의 PxlStar() 함수에서 합니다. for문 안에서 각 방향에 따라 샘플링을 하고 이 값을 계속 누적해 나갑니다.

 

float4 PxlStar(SvsOut In) : COLOR0

        for(int i=0; i<MAX_SAMP; ++i)

        {

               uv = In.Tex + float2(m_StarVal[i].x, m_StarVal[i].y);

               Out += tex2D(smpDif, uv) * m_StarVal[i].z;

        }

 

 

<고휘도 검출, 6방향 늘리기. 하드웨어가 괜찮다면 방향 늘리기 전 흐림 효과를 1~2회 적용한다>

 

6방향으로 픽셀을 늘리고 다음으로 흐림 효과를 적용해서 중간에 끊길지도 모르는 부분을 채워 줍니다.

마지막으로 6장의 빛을 모아 하나의 텍스처에 저장합니다.

 

float4 PxlStarAll(SvsOut In) : COLOR0

        float4  Out=0;

        Out += tex2D(s0, In.Tex);

        Out += tex2D(s5, In.Tex);

 

이 저장된 텍스처와 마지막에 전체 장면과 합칩니다.

 

float4 PxlAll(SvsOut In) : COLOR0

{

        float4  Out=0;

        float4  t0 = tex2D(smpDif, In.Tex);

        float4  t1 = tex2D(smpDif2, In.Tex);

        Out = t0*0.8f + t1*.3f;

        Out.w = 1;

        return Out;

}

 

 

<크로스 필터 효과: h4_06_cross1.zip>

 

빛은 파장에 따라 굴절 되는 각도가 다릅니다. 크로스 필터에도 이것을 간단하게 적용하는 방법은R, G, B에 따라 샘플링 되는 지점의 U, V에 조금씩 차이를 주면 색수차를 만들어 낼 수 있습니다.

다음은 붉은 색에 대한 색수차 적용입니다.

 

float4 PxlStarR(SvsOut In) : COLOR0

        for(int i=0; i<MAX_SAMP; ++i)

        {

               u = In.Tex.x + m_StarVal[i].x*1.25f;

               v = In.Tex.y + m_StarVal[i].y*1.25f;

               Out.r += tex2D(smpDif, float2(u,v)) * m_StarVal[i].z;

        }

 

이것을 녹색, 파란색에도 적용하면 다음과 같은 화면을 얻을 수 있습니다.

 

 

<색수차가 적용된 크로스필터: h4_06_cross2.zip>

 

이 정도 연습했으면 3D 장면에 적용해볼 차례입니다. 작업은 다음 그림처럼 진행이 될 것입니다.

 

<크로스 필터: 3D 장면 적용>

 

지난 시간에 파티클을 이용해서 화면의 왜곡을 만들어 본 적이 있습니다. 그 때에 왜곡시키는 요소를 따로 텍스처에 저장을 하고 해당 효과를 적용했습니다. 지금도 마찬가지로 3D 장면과 휘도를 적용 대상을 따로 렌더링 해야 합니다.

여기서는 특별한 오브젝트 추가 없이 나무가 고휘도를 만드는 오브젝트라 가정하고 이 들을 가지고 크로스 필터 효과를 만들어 보겠습니다. 먼저 1번 단계의 전체 장면과 고휘도 대상인 2번의 나무만 렌더링을 각각 진행 합니다. 2번 나무만 렌더링 한 텍스처를 가지고 크로스 필터를 만듭니다.

4번 단계에서 이 둘을 혼합 합니다.

 

CMain클래스에서 전체 장면과 휘도 장면을 저장하기 위해 다음 2개의 멤버를 선언합니다.

 

IrenderTarget* m_pTrnd0;      // 전체 장면

IrenderTarget* m_pTrnd1;      // 고휘도 적용 장면

 

전체 장면은 화면 크기대로 만들고 휘도로 사용할 텍스처는 렌더링 속도 때문에 축소해서 만듭니다.

 

HRESULT CMain::Init()

// 3D 장면 저장 텍스처 생성

LcD3D_CreateRenderTarget(NULL, &m_pTrnd0, m_pd3dDevice);

// 고휘도 저장 텍스처 생성

FLOAT fTexW = 256;

FLOAT fTexH = 256;

LcD3D_CreateRenderTarget(NULL, &m_pTrnd1, m_pd3dDevice, fTexW, fTexH);

 

이들을 매 프레임 마다 렌더링을 합니다. 그리고 CShaderEx 클래스 객체에 이 두 텍스처를 전달합니다.

 

HRESULT CMain::FrameMove()

// Rendering Target에 장면을 그린다.

m_pTrnd0->BeginScene();

        m_pd3dDevice->Clear(…);

        RenderScene(0);

m_pTrnd0->EndScene();

 

// 휘도 부분에 대한 장면을 그린다.

m_pTrnd1->BeginScene();

        m_pd3dDevice->Clear(…);

        RenderScene(1);

m_pTrnd1->EndScene();

// CShaderEx 클래스에 장면, 휘도 텍스처 연결

LPDIRECT3DTEXTURE9 pTx0 = (LPDIRECT3DTEXTURE9)m_pTrnd0->GetTexture();

LPDIRECT3DTEXTURE9 pTx1 = (LPDIRECT3DTEXTURE9)m_pTrnd1->GetTexture();

m_pShader->SetSceneTexture(pTx0);

m_pShader->SetLuminescenceTexture(pTx1);

SAFE_FRMOV(    m_pShader      );

 

CMain에서 할 일은 끝났습니다. 다음으로 CShaderEx 클래스의 과정입니다. CShaderEx 클래스에서는 CMain에서 전달한 3D 장면과 휘도에 대한 텍스처 포인터 2개를 가지고 있어야 합니다. 다음으로 크로스 필터에 대한 텍스처 9장을 준비합니다. 이것은 순서를 잘 맞추면 더 줄일 수 있습니다. 일단 여기서는 각 장면이 제대로 처리되는지 확인을 위해 넉넉하게 사용하겠습니다.

 

PDTX           m_pTexS;       // Scene Texture

PDTX           m_pTexL;       // Luminescence Texture

IrenderTarget* m_pTrndX;      // Rendering Target Texture for Blur X

IrenderTarget* m_pTrndY;      // Rendering Target Texture for Blur Y

IrenderTarget* m_pTrndC[6];   // Cross Texture

IrenderTarget* m_pTrndS;      // Cross Texture All

 

적당한 크기를 할당해 텍스처를 만들고 매 프레임 마다 크로스 필터 효과를 만들어 냅니다. 이전과 달라진 부분은 1번에서 휘도용 텍스처를 사용해야 하는 것과 크로스 모양을 만들기 전에 동적인 움직임을 주기 위해 약하게 시간에 따라 별 모양을 회전 시켰습니다.

 

INT CShaderEx::FrameMove()

// 1. Luminescence Texture를 사용한다.

        pTx = m_pTexL;

// 3. 축소된 텍스처를 뭉갠다.

D3DXMATRIX mtViw;

m_pDev->GetTransform(D3DTS_VIEW, &mtViw);

mtViw._41 = 0;    mtViw._42 = 0;    mtViw._43 = 0;

 

D3DXQUATERNION q;

FLOAT   fTime=GetTickCount()*0.001f;

D3DXQuaternionRotationMatrix(&q, &mtViw);

float fc= acosf(q.w*0.99f) * 24.f + fTime*0.1f;

 

//4. Cross를 만든다.

for(j=0; j<6; ++j)

        float   fTheta = (fc+2.f * j * D3DX_PI)/6;

 

이전 코드를 거의 그대로 사용하지만 각 단계별로 원하는 장면이 나오는지 중간중간 점검합니다.

CShaderEx::Render() 함수의 #if 1 #if 0 으로 하고 주석 처리된 부분을 다시 활성화 시키면 각 단계별 장면을 화면에 출력합니다.

 

// 전체 장면

//m_pDev->SetTexture(0, m_pTexS);

//m_pDev->DrawPrimitiveUP( D3DPT_TRIANGLEFAN, 2, pVtx, sizeof(VtxDUV1));

 

// 고휘도 장면

//m_pDev->SetTexture(0, m_pTexL);

//m_pDev->DrawPrimitiveUP( D3DPT_TRIANGLEFAN, 2, pVtx, sizeof(VtxDUV1));

 

// 크로스 필터 장면

pTex = (PDTX)m_pTrndY->GetTexture();

m_pDev->SetTexture(0, pTex);

m_pDev->DrawPrimitiveUP( D3DPT_TRIANGLEFAN, 2, pVtx, sizeof(VtxDUV1));

 

다음 그림은 각 단계별 장면입니다.

 

<크로스 필터 휘도 추출: h4_06_cross2_a.zip>

 

쉐이더 파일 "hlsl.fx" 파일에서 장면과 크로스 필터를 섞는 PxlAll() 함수의 마지막 단계에서 붉은색, 초록색, 파란색의 비율을 다르게 적용하면 다음 그림과 같은 장면을 만들어 냅니다.

 

float4 PxlAll(SvsOut In) : COLOR0

//      Out = t0*1.5f + t1*.4f*float4(1.5, 0.3, .2, 1);

//      Out = t0*1.5f + t1*.4f*float4(.6, 1., .8, 1);

//      Out = t0*1.5f + t1*.4f*float4(.3, 0.7, 1.6, 1);

        Out = t0*1.5f + t1*.4f*float4(1.3, .3, 1.4, 1);

 

 

 

<3D 장면에서의 크로스 필터: h4_06_cross2_a.zip>

 

 

4.5.4 외곽선 추출

앞의 흐림 효과(Blur Effect)가 수학으로 말하면 색상에 대한 적분이라 할 수 있습니다. , 주변의 색상을 해서 평균을 내는 것입니다. 외곽선 추출을 반대로 미분에 해당합니다. 미분은 간단히 정의하면 변화 량(Δ)이라 할 수 있습니다. 예를 들어 색상을 [0, 255]로 표현할 때 회색 10과 회색 50은 변화 량이 40이지만 회색 250과 회색 248은 변화 량이 2입니다.

단순히 색상만 가지고 화면에 출력하면 회색 250 248은 거의 하얀색으로 나타나고 회색 10 20은 거의 검정색으로 나오겠지만 변화 량을 화면에 표현하면 10 20이 더 밝게 나올 것이라는 것은 당연합니다.

그럼 이 변화 량은 어떻게 만들어야 할까요?

가장 빠른 방법은 마스크를 이용하는 것입니다. 마스크는 하나의 픽셀에 대하여 이웃한 픽셀 들에 대해서 정방 행렬 형태로 가중치를 지정한 값으로 외곽선 추출에서는 소벨(Sobel), 라플라시안(Laplacian) 마스크를 주로 사용합니다.

다음 그림은 소벨 마스크의 X, Y 방향 마스크와 라플라시안 마스크입니다.

 

,   

<소벨(Sobel), 라플라시안(Laplacian)>

 

소벨 마스크는 윤곽선 검출의 가장 대표적인 마스크로 X, Y 두 방향에 대한 필터가 있어 이들을 각각 적용한 후에 최종 기울기(Gradient)를 결정합니다. 이 기울기를 수식으로 표현하면 다음과 같습니다.

 

 

앞의 그림의 마스크 값을 이용해서 쉐이더 코드로 작성한다면 다음과 같습니다.

 

float4 PxlSobel(SvsOut In) : COLOR0

{

        float2 Offset[8] =

        {

                {-1,-1}, { 0,-1}, { 1,-1},

                {-1, 0},          { 1, 0},

                {-1, 1}, { 0, 1}, { 1, 1},

        };

 

        float4  Out=float4(0,0,0,1);

        float4  Mon=float4(0.299, 0.587, 0.114 , 0);

        float2  uv;

        float4  Tex[8];

        float4  TexVert;

        float4  TexHorz;

       

        for(int i =0; i < 8; i++)

        {

               Offset[i].x /= m_TexW;

               Offset[i].y /= m_TexH;

 

               uv = In.Tex + Offset[i];

               Tex[i] = tex2D( smpDif, uv);

               // convert to Mono

               Tex[i] = dot(Tex[i], Mon);

        }

       

        // Vertical

        TexVert = -(Tex[0] + Tex[5] + 2*Tex[3]);

        TexVert += (Tex[2] + Tex[7] + 2*Tex[4]);

       

        // Horizontal

        TexHorz = -(Tex[0] + Tex[2] + 2*Tex[1]);

        TexHorz += (Tex[5] + Tex[7] + 2*Tex[6]);

       

        Out = sqrt( TexHorz*TexHorz + TexVert*TexVert );

        Out = saturate(Out);

        Out *= 4;

        Out.w = 1.f;

        return Out;

}

 

이 쉐이더 코드를 적용하면 다음과 같은 장면을 얻을 수 있습니다.

 

 

<소벨(Sobel) 마스크에 의한 윤곽선>

 

소벨 마스크와 함께 또한 가장 많이 사용되는 마스크는 라플라시안(Laplacian) 입니다. 이 마스크는 이론적으로 2차 미분 연산자 사용하며 한 번에 하나의 마스크로 윤곽선 검출 수행 연산 속도가 매우 빠르다는 것이 가장 큰 장점입니다.

결과는 다른 연산자와 비교하여 날카로운 윤곽선을 만들어 내는 것이 특징입니다. 변화에 대한 (G: Gradient)는 각 변화에 대한 합으로 다음과 같이 간단하게 계산합니다.

 

 

쉐이더 코드는 다음과 같이 작성합니다.

 

float4 PxlLaplacian(SvsOut In) : COLOR0

{

        float4  Mono={0.299, 0.587, 0.114, 0};

        float3 Laplacian[9] =

        {

                {-1,-1, -1}, { 0,-1, -1}, { 1,-1, -1},       // offset X, offset y, Weight

                {-1, 0, -1}, { 0, 08}, { 1, 0, -1},

                {-1, 1, -1}, { 0, 1, -1}, { 1, 1, -1},

        };

        float4  Out=float4(0,0,0,1);

        float4  txC=0;

        float2  uv;

        float   x, y, w;

 

        for(int i=0; i<9; ++i)

        {

               x = Laplacian[i].x/m_TexW;

               y = Laplacian[i].y/m_TexH;

               w = Laplacian[i].z;

               uv  = In.Tex + float2(x, y);

               txC+= tex2D(smpDif, uv)*w;

        }

 

        float d=dot(txC, Mono);

        d *=30;

        Out.xyz = d;

        return Out;

}

 

  

<라플라시안(Laplacian) 마스크에 의한 윤곽선>

 

이렇게 외곽선 추출은 이웃한 색상의 변화 량(Gradient)를 조사해서 이 값의 크기를 색상으로 결정하는 것입니다. 외곽선 추출이 이런 내용이라면 마스킹 값을 이용하지 않고 직접 변화 량을 계산해서 적용해 볼 수 있습니다. 지금 소개하려는 방법은 색상 r, g, b 3차원 좌표 x, y, z처럼 생각해서 두 색상의 변화 량을 으로 계산해 보자는 것입니다. 이 방법은 앞의 방법보다 제곱 근 계산이 들어가서 무겁지만 본래의 의도에 가장 충실한 방법이 될 것입니다.

 

 

이것은 다음과 같이 쉐이더 코드로 쉽게 표현할 수 있습니다.

 

float4 PxlDist(SvsOut In) : COLOR0

{

        float4  Out=float4(0,0,0,1);

        float2  uv;

        float3  txI;

        float3  txC;

        float   D=0;

 

        txI = tex2D(smpDif, In.Tex);

 

        for(int j=-1; j<=1; ++j)

        {

               for(int i=-1; i<=1; ++i)

               {

                       if(!(0==i && 0==j))

                       {

                              uv = In.Tex + float2(i/m_TexW, j/m_TexH);

                              txC     = tex2D(smpDif, uv);

                              txC     -= txI;

                              D       += length(txC);

                       }

               }

        }

 

        D *=3;

        if(D>1.)

               Out.rgb = 1;

 

        return Out;

}

 

코드에서 먼저 중심 픽셀의 색상을 txI = tex2D(smpDif, In.Tex) 으로 구한 후에 for 문에서 length()함수로 색상 차이에 대한 길이를 구하고 있습니다. length()함수는 내부에서 sqrt()함수를 호출해서 길이를 계산하는 함수 입니다. 코드의 내용은 두 픽셀의 색상차이를 길이로 저장하고 있습니다.

이 쉐이더 코드를 가지고 실행하면 다음과 같이 색상의 변화에서 이전의 방식 보다 좀 더 뚜렷하게 만들어 갈 수 있습니다.

 

<색상의 거리를 이용한 외곽선 추출>

 

예제 h4_07_out_line1.zip 를 실행하면 처음에는 거리를 이용한 외곽선 추출을 볼 수 있습니다. 다음으로 'S' 또는 'L' 키를 누르면 각각 소벨 마스크, 라플라시안 마스크를 적용한 외곽선 추출을 볼 수 있습니다.

 

이러한 외곽선 추출은 카툰 쉐이딩, 수묵화, 색상의 영역 설정 등에서 두루 사용이 됩니다. 다음의 h4_07_out_line2.zip 는 완벽한 수묵화 기법은 아니지만 외곽선만 조정해서 수묵화 느낌을 살린 예제입니다.

 

<외곽선을 이용한 수묵화 h4_07_out_line2.zip>

 

이 장면은 다음 그림과 같은 과정을 통해서 만든 것입니다.

 

<h4_07_out_line2.zip 예제 제작 과정>

 

3D 장면을 저장한 텍스처에서 외곽선을 추출합니다. 여기서 사용한 외곽선은 거리를 이용한 방법입니다. 외곽선을 그대로 사용하면 좁고, 날카롭습니다. 붓 터치 느낌을 만들기 위해 흐림 효과를 반복 적용합니다. 흐림 효과를 반복 적용하면 밝기가 많이 낮아집니다. 따라서 강하게 색상을 반전 시키면 먹물 느낌을 만들어 낼 수 있습니다. 준비한 배경 텍스처와 혼합하면 앞의 장면을 만들어 낼 수 있습니다.

h4_07_out_line2.zipCShaderEx::FrameMove() 함수에서 원 장면에서 외곽선을 추출하고 흐림 효과를 적용하고 있습니다. CShaderEx::Render() 함수에서는 강하게 반전(Inversion)한 후에 배경 텍스처와 혼합하는 작업을 하고 있습니다.

 

 

4.5.5 수묵과 렌더링

수묵화 렌더링 기법은 조명과 외곽선 추출을 이용한 방법입니다. 보통 조명에서 밝은 부분은 밝게 어두운 부분은 어둡게 처리하지만 수묵화에서는 이와 반대로 밝은 부분은 어둡게 어두운 부분은 밝게 처리합니다. 이렇게 해야만 먹물로 그림을 그린 느낌을 만듭니다. 붓에 의한 번짐을 표현하기 위해서 밝고 어둠의 반전을 만든 후에 흐림 효과를 적용합니다. 이렇게 만든 것을 텍스처에 저장을 합니다.

다음으로 외곽선을 렌더링 오브젝트에서 추출합니다. 이 때 깔끔한 외곽선을 만들기 위해서 색상의 변화를 가장 잘 만들 수 있는 오브젝트의 법선 값, 또는 깊이 값을 색상으로 저장해서 이 저장된 텍스처에서 외곽선을 추출합니다. 추출한 텍스처를 역시 흐림 효과를 적용 합니다. 마지막으로 앞서 반전을 이용해 만든 구한 텍스처와 외곽선을 만든 텍스처와 혼합합니다.

 

이 과정을 그림으로 표현하면 다음과 같습니다.

 

<수묵화 렌더링 과정>

 

앞에서 인접한 색상의 차이가 클 수록 외곽선이 잘 표현된다는 것을 알았습니다. 주변 색상과 의도적으로 차이를 만들기 위해 오브젝트의 깊이 값 또는 법선 벡터를 텍스처에 저장해서 사용하는데 여기서는 깊이 값을 사용하도록 하겠습니다.

 

깊이 값을 저장하는 방법은 간단합니다. 먼저 정점 처리과정에서 위치에 대한 변환 후의 값을 텍스처 좌표 값으로 저장합니다. 다음으로 픽셀 처리과정에서 쉐이더로 이 값을 실시간으로 만든 텍스처에 저장합니다.

 

깊이 값을 저장하기 위해서 정점 쉐이더는 다음과 같이 작성합니다.

 

float4x4       m_mtWVP;               // World * View * Projection

// 버텍스 쉐이더 출력

struct SvsOut

{

        float4 Pos : POSITION;

        float4 Nrp : TEXCOORD7;               // Normal vector+ 깊이 값

};

 

SvsOut InkVtx(float3 Pos : POSITION)

{

        SvsOut Out = (SvsOut)0;

        float4 P = mul(float4(Pos,1), m_mtWVP);              // 정점 위치 변환

        Out.Nrp.w      = P.z;

        return Out;

}

 

보통 정점 처리 과정에서 만든 데이터를 픽셀 쉐이더로 보낼 때 참조가 안 되는 값들은 텍스처 좌표 데이터(TEXCOORD7~0)를 선택해서 사용합니다. 예를 들어 정점의 위치를 픽셀 쉐이더로 전달할 수 있지만 값은 읽지 못합니다. 이런 이유로 앞의 쉐이더 코드는 변환 후의 정점의 깊이 값을 7번 인덱스 텍스처 좌표의 w 값에 저장한 것입니다. 또한 깊이 값만 사용하고 있어서 새로운 텍스처 인덱스를 부여하면 다른 곳에서 사용할 수 있는 폭이 줄어 들기 때문입니다.

 

픽셀 쉐이더는 정점의 깊이 값을 텍스처에 쓰기만 하면 될 것 같지만 변환 후의 깊이 값은 거의 1근처에 몰려 있습니다. 따라서 이 값을 적당한 값으로 나누어야 하는데 대략 뷰 체적(View Volume) Far 평면까지의 거리 값을 사용해도 되고, 색상의 범위가 [0, 255] 이므로 이것의 2 배 정도 되는 500 정도 되는 값을 사용해도 됩니다.

 

float4 InkPxlDepth(SvsOut In) : COLOR0

{

        float4  Out = 1;

        float4  Nrp = In.Nrp;

        float   d;

 

        d = Nrp.w;

        d /=500.f;

        Out.xyz = d;

        return Out;

}

 

깊이 값을 텍스처에 저장할 수 있으니 이제 외곽선 만드는 것이 남았습니다. 이 과정은 예제 h4_07_out_line2_ink1_edge.zip에 순서대로 구현되어 있습니다.

먼저 깊이 값을 저장하고 이것을 외곽선으로 사용하기 위해서 다음과 같이 깊이 값 텍스처와 임시 버퍼를 준비합니다.

 

IrenderTarget* m_pTrndEdg;            // Silhouette Edge Texture for Scene

IrenderTarget* m_pTrndTmp;            // Blurring for Temp

 

이들은 화면 크기와 동일하게 만듭니다. 다음으로 깊이 값을 저장해야 합니다. CShaderEx:: FrameMove() 함수의 중간에 보면 다음과 같은 코드로 임시 버퍼에 깊이 값을 저장하고 있는 것을 볼 수 있습니다.

 

// 텍스처에 깊이 값을 임시 버퍼에 쓴다.

m_pTrndTmp->BeginScene((0x1L|0x2L), 0xFFffffff);

        hr = m_pDev->SetVertexDeclaration( m_pFVFN ); // 정점 선언

        hr = m_pEft->SetMatrix( "m_mtWVP", &mtWVP);  // 월드**프로젝션 변환 행렬

        hr = m_pEft->SetTechnique( "TechInk");

        hr = m_pMesh->DrawSubset( 0 );

m_pTrndTmp->EndScene();

 

// Silhouette , 외곽선을 추출한다.

pTx = (PDTX)m_pTrndTmp->GetTexture();

m_pTrndEdg->BeginScene();

        hr = m_pDev->SetVertexDeclaration( m_pFVFD );

        hr = m_pEft->SetTechnique( "TechInk");

        hr = m_pDev->SetTexture( 0, pTx);

        hr = m_pDev->DrawPrimitiveUP(…);

m_pTrndEdg->EndScene();

 

코드의 내용은 먼저 임시 버퍼에 오브젝트의 깊이 값을 기록한 후에 외곽선을 추출하고 있습니다. 외곽선 추출은 "hlsl.fx"InkPxlEdge() 함수로 구현되어 있는데 소벨(Sobel) 마스크를 이용했는데 이 부분의 설명은 앞의 외곽선 추출을 참고 하기 바랍니다. 이렇게 외곽선 추출이 끝나면 이를 그대로 사용해도 되나 먹물의 번짐 효과를 고려해서 흐림 효과를 적용합니다.

임시 버퍼에 기록된 깊이 값과 외곽선 추출 후, 흐림 효과 적용에 대한 화면 출력은 다음과 같습니다.

 

  

<h4_07_out_line2_ink1_edge.zip: , 외곽선 추출, 흐림 효과 적용>

 

2단계의 몸체에 대한 처리는 조명의 반사에 대한 밝기를 이용합니다. 일반적으로 3D에서 밝은 부분은 밝게 처리하지만 수묵화에서는 이를 반대로 처리해서 밝은 부분에는 먹물을 진하게 적용하고 어두운 부분은 엷게 처리합니다.

또한 반사의 밝기는 Lambert 분산 값과 Phong 반사 값을 더해서 사용하지만 수묵화에서는 이 둘을 곱해 버립니다. 이렇게 되면 밝은 부분과 어두운 부분의 차이가 극대화 되어 부수적으로 여백의 미()도 어느 정도 표현이 됩니다.

 

이 정도의 지식만으로 몸체에 대한 색 처리를 할 수 있는데 여기에 하나 더 카툰(Cartoon) 쉐이딩에서 사용한 기법을 적용하면 농담의 변화가 연속이 아닌 이산(Discrete) 값으로 나타나 붓 느낌을 살립니다. 또한 밝기에 대한 변화를 카툰 쉐이딩의 반대로 다음과 같이 밝은 부분은 어두운 곳이 샘플링 되고 반대로 어두운 부분은 밝은 곳이 샘플링 되도록 만듭니다. 그리고 의도적으로 흰색 부분을 많이 넣어서 여백의 미를 만들 수 있도록 합니다.

 

<수묵화 쉐이딩 텍스처>

 

쉐이더 작성은 카툰 쉐이더와 같습니다. 단지 Lambert 확산 값(Dif) Phong 반사 값(Spc)을 곱하고 수묵화 쉐이딩 텍스처에서 샘플링 하는 것이 차이 입니다.

 

float4 InkPxlInv(SvsOut In) : COLOR0

{

        float4  Out = 1;

        Nor.xyz = Nrp.xyz;

        Nor     = normalize(Nor);

 

        Dif     = saturate( dot(Nor, Lgt));

 

        Spc     = saturate((dot(Rfc, Eye) + 1) *.5);

        Spc     = pow( Spc, m_fShrp);

        Spc    *= 7;

 

        // Lambert 값과 Phong 반사 값을 곱한다.

        Out = Dif * Spc;

 

        // 수묵화 쉐이딩 텍스처에서 샘플링 한다.

        Out = tex2D(smp0, float2(Out.x, 0.5f));

 

        return Out;

}

 

이렇게 수묵화 쉐이딩 텍스처를 처리하고 나서 흐림 효과를 적용하면 다음의 오른쪽 그림을 얻을 수 있습니다.

 

  

<h4_07_out_line2_ink2_ink.zip: 일반 조명, 수묵화 텍스처, 흐림 효과>

 

이제 앞의 외곽선과 합치는 일만 남아 있습니다. 둘을 합치고 배경 텍스처를 혼합하면 다음과 같은 장면을 만들어냅니다.

 

 

<h4_07_out_line2_ink3.zip>



Copyright (c) 2004 3dapi.com All rights reserved.

Creative Commons License