home lecture link bbs blame

◈HLSL 연습◈

1 HLSLID3DXEffect

1.1 이펙트 기초

우리는 지금까지 정점 쉐이더와 픽셀 쉐이더를 어셈블리 형식의 저 수준 쉐이더 언어를 C언어와 같은 고 수준 언어 HLSL로 각각 독립적으로 작성해서 적용해왔습니다. 그런데 쉐이더를 사용한다는 것은 프로그램 가능한 파이프라인을 이용하는 것을 의미하고 정점과 픽셀을 독립적으로 각각 처리하기 보다 동시에 처리하는 것을 염두 해 두는 것입니다. 물론 정점 쉐이더와 픽셀 쉐이더를 구분해서 컴파일을 하고, 독립적으로 사용하는 것이 각각 다른 그래픽 카드의 성능에 대처할 수 있는 유연성이 있어 보이지만 이것도 통합적으로 작성해서 각 상황에 맞게 처리할 수 있는 방법으로 해결하는 것이 나을 수 있습니다.

현재 대부분의 그래픽 카드들은 픽셀 쉐이더를 지원하는 경우라면 정점 쉐이더는 당연히 지원이 되고 프로그램에서도 픽셀 쉐이더를 분리해서 사용할 필요가 없기 때문에 이를 한꺼번에 처리하고 코드의 구현에서 좀 더 편리성을 위한 객체가 필요합니다. 다행히도 마이크로소프트는 Effect 객체를 제공합니다. 이것은 어셈블리 + HLSL + α 이상의 내용을 담고 있어서 고정파이프라인에서 보다 다양한 3D 장면을 연출할 수 있게 해주고 있습니다.

 

<ID3DXEFfect 객체의 역할>

 

D3D Effect는 위의 그림처럼 저 수준 쉐이더와 고 수준 쉐이더를 기본적으로 지원하고 작업의 편리성을 위한 상수의 공용화, 정점 처리 상태 설정, 픽셀 처리 상태 설정 등의 옵션(Option) 들이 추가된 형태입니다.

기본적인 문법은 이전의 저 수준, 고 수준과 같고 정점 처리와 픽셀 처리는 각각 작성하고 컴파일방법을 지정합니다. ef01_basic1_hlsl1.zip 예제의 “data/hlsl.fx” 파일은 ID3DXEffect를 위한 쉐이더이며 코드를 보면 정점과 픽셀 처리의 main 함수 작성은 이전에 배운 HLSL과 동일하고 마지막에 “technique”“pass”안에서 컴파일을 지정하고 있음을 볼 수 있습니다.

키워드 "technique" 은 정점과 픽셀 처리에 대한 단위이고, "pass"는 정점 처리와 픽셀 처리의 main 함수와 렌더링의 상태 설정입니다.

하나의 테크닉은 여러 패스를 가질 수 있고, 전체 쉐이더 코드는 여러 개의 테크닉을 가질 수 있습니다.

 

technique Tech0

{

        pass P0

        {

               VertexShader = compile vs_1_1 VtxPrc();

               PixelShader  = compile ps_1_1 PxlPrc();

        }

        pass P1

        …

}

 

technique Tech1

{

 

이렇게 여러 개의 Pass Technique의 지원은 패스에서 여러 개의 쉐이더 함수를 조합해서 하나의 패스를 만들고, 이를 테크닉은 여러 패스를 설정 할 수 있어서 단순한 HLSL보다 조합에 의한 다양한 연출을 실행 프로그램에서 만들지 않고 쉐이더 언어를 작성하는 곳에서 결정을 할 수 있게 됩니다. 또한 반복적인 쉐이더 내용을 분리해 작성할 수 있어서 모듈의 응집력이 높아집니다.

 

이렇게 작성된 고 수준 쉐이더 언어는 D3DXCreateEffect…() 함수로 컴파일 하며 컴파일과 동시에 ID3DXEffect 객체를 생성합니다.

 

D3DXCreateEffectFromFile( m_pd3dDevice, "data/hlsl.fx"

               , NULL, NULL, dwFlags, NULL

               , &m_pEft, &pErr);

 

HLSL, 쉐이더와 차이점은 컴파일과 동시에 이펙트 객체를 생성하고 있고, 상수 테이블을 만들지 않고 있습니다. 나머지 에러에 대한 처리는 저수준 쉐이더, HLSL과 동일합니다.

ID3DXEffect 객체는 내부에 상수를 설정할 수 있도록 구성되어 있으며 인터페이스는 상수 테이블에서 사용한 함수들을 그대로 사용할 수 있고 Technique, Pass, 텍스처에 대한 인터페이스가 추가되어 있습니다.

 

ID3DXEffect로 장면을 연출하는 과정은 SetVector(), SetMatrix(), SetInt() 등의 함수로 상수를 설정하고, SetTechnique()함수로 Technique을 지정합니다. 다음으로 Begin()함수를 이용해서 Pass 개수 확인합니다. 마지막으로 Pass()(2003버전), 또는 BeginPass() / EndPass()(2003 이후 버전) 함수 사이에 장면 연출 함수 Draw…()를 호출합니다. Pass(), BeginPass() 함수는 Pass안에 구성된 정점 쉐이더와 픽셀 쉐이더 객체 사용을 지정하는 것과 같습니다.

구체적으로 각 단계에 대한 예를 보이겠습니다. 먼저 행렬, 벡터 등의 상수 값들은 상수 테이블에서 사용했던 방식 그대로 다음과 같이 사용합니다.

 

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

m_pEft->SetMatrix("m_mtViw", &mtViw);

m_pEft->SetMatrix("m_mtPrj", &mtPrj);

 

Pass들의 집합인 Technique SetTechique() 함수로 지정합니다.

 

m_pEft->SetTechnique("Tech0");

 

다음으로 Technique에 지정된 Pass의 숫자를 Begin()함수로 얻습니다. Pass의 개수가 필요 없다면 NULL 값을 전달합니다.

 

m_pEft->Begin( &nPass, 0 );   // m_pEft->Begin(NULL, 0 );

m_pEft->End();

 

Begin() 함수는 End()함수와 반드시 짝을 이루어야 합니다. 마지막으로 Begin()/End()함수 사이에 다음과 같이 for 문 또는 직접 인덱스를 사용해서 Pass() 함수 (2003 이후 버전 BeginPass() / EndPass()) 아래에 Draw…() 함수를 호출합니다. Pass(), 또는 BeginPass()/EndPass() 함수는 Technique 안의 Pass에 지정된 정점 쉐이더와 픽셀 쉐이더 사용을 지정하는 것과 같습니다.

 

for(UINT n = 0; n < nPass; ++n)

{

        m_pEft->Pass( n );// 2003 이후 버전 m_pEft->BeginPass()

        m_pDev->DrawPrimitiveUP( D3DPT_TRIANGLELIST, 1, m_pVtx, sizeof(VtxD));

       

        // m_pEft->EndPass(); // 2003 이후 버전

}

 

m_pEft->End(); //m_pEft->Begin() 과 대응

 

End()함수를 호출하면 정점 쉐이더, 픽셀 쉐이더 사용이 끝나야 되는데 간혹 그래픽 카드에서 처리되지 않을 수 있습니다. 다음과 같이 명시적으로 쉐이더 사용을 해제합니다.

 

m_pDev->SetVertexShader( NULL);

m_pDev->SetPixelShader( NULL);

 

<간단한 ID3DXEffect 사용: ef01_basic1_hlsl1.zip>

 

 

1.2 Multi-Techniques

1.2.1 Effect의 상태 설정과 저 수준 쉐이더

Pass는 렌더링 처리과정의 가장 기본 단위이며 보통 Pass안에 정점 쉐이더 또는 픽셀 쉐이더 객체를 지정합니다. 그런데 쉐이더 객체를 지정하지 않으면 고정 기능 파이프라인의 정점 처리, 픽셀 처리, 그리고 기타 렌더링 머신 값을 설정 하게 됩니다. 이러한 이점은 다양한 렌더링 환경에 대해서 게임 프로그래머가 특별한 자료 구조를 안 만들어도 되며 범용성이 있어서 다른 게임 프로그램에도 수정 없이 적용할 수 있게 됩니다.

 

technique Tech0

{

        pass P0

        {

               // Setup Vertex Processing Constant of Rendering Machine

                FOGENABLE      = FALSE;

               LIGHTING       = FALSE;

               CULLMODE       = CCW;

 

               // Setup Vertex Processing Constant of Rendering Machine

               Sampler[0]     = (SmpDif0);

               ColorOp[0]     = ADDSIGNED;

               ColorArg1[0]   = TEXTURE;

               ColorArg2[0]   = DIFFUSE;

               AlphaOp[0]     = DISABLE;

               ColorOp[1]     = DISABLE;

        }

        pass P1

 

  

< 고정 기능 파이프라인의 상태 값 설정: ef01_basic1_hlsl2.zip - 숫자 1, 2, 3 >

 

D3DX EffectC 언어의 inline assembly 와 같이 저 수준과 고 수준 언어를 혼용해서 사용할 수 있습니다. C 언어는 함수의 중간에 __asm 키워드를 사용해서 inline assembly를 종속적으로 구현하지만 Effect는 정점 쉐이더(Vertex Shader) 객체, 픽셀 쉐이더(PixelShader) 객체를 독립적으로 작성하고 이를 Pass안에서 지정 하는 형태입니다.

저 수준 쉐이더를 Effect에서 작성하는 방법은 크게 2가지 입니다. 첫 번째 방법은 다음과 같이 technique 밖에서 정점 쉐이더, 또는 픽셀 쉐이더를 작성하고 pass 안에서 지정하는 것입니다.

 

float4x4       m_mtWVP;       // 월드 * * 투영 행렬

float3x3       m_mtWld;       // 월드 행렬

float3          m_vcLgt;       // 광원 방향 벡터

 

// 정점 처리의 저 수준 작성

VertexShader VtxPrc = asm

{

        vs_1_1

        dcl_position  v0

        dcl_normal    v1

        dcl_texcoord  v2

        m4x4 oPos, v0, c0

        m3x3 r0,   v1, c4

        dp3  r1,   r0,-c8

};

 

// Pixel Processing

PixelShader PxlPrc = asm

{

        ps_2_0

};

 

technique Tech0

{

        pass P0

        {

               // 저 수준 쉐이더에 상수 연결

               VertexShaderConstant4[0]  = (m_mtWVP);

               VertexShaderConstant4[4]  = (m_mtWld);

               VertexShaderConstant1[8]  = (m_vcLgt);

 

               // 정점 쉐이더 객체 지정

               VertexShader = (VtxPrc);

               // 픽셀 쉐이더 객체 지정

               PixelShader = (VtxPrc);

 

 

<Effect 안에서 저 수준 쉐이더1: ef01_basic2_asm1_1.zip, ef01_basic2_asm1_2.zip >

 

저수준 안에서 사용되는 상수는 pass에서 VertexShaderConstant{1….4}[index], PixelShaderConstant{1….4}[index] 등으로 지정합니다. 예를 들어 4x4행렬을 상수 레지스터 c10에 연결하고자 한다면 VertexShader4[10] = "행렬"; 식으로 작성합니다. 상수 연결과 고정 기능 파이프라인의 연결은 SDK 도움말의 Effect States 부분을 살펴 보기 바랍니다.

저 수준 쉐이더를 사용하는 두 번째 방법은 pass안에 직접 작성하는 것입니다. 상수 설정 등의 Effect의 상태는 이전과 동일합니다.

 

float4  m_cv[6];       // 정점 변환 행렬과 텍스처 애니메이션 좌표

float4  m_cp[8];       // 픽셀 쉐이더 상수

 

technique Tech0

{

    pass P0

    {

        VertexShaderConstant1[0] = (m_cv[0]);

        PixelShaderConstant1[0]  = (m_cp[0]);

        VertexShader = asm

        {

               vs_1_1

        };

        PixelShader = asm

        {

               ps_1_4

 

 

< Effect 안에서 저 수준 쉐이더2: ef01_basic2_asm2_1.zip, ef01_basic2_asm2_2.zip>

 

지금까지의 내용을 종합해 보면 technique은 렌더링 상태 머신 값을 설정하고, 특히 쉐이더 객체가 지정되지 않으면 고정 기능 파이프라인의 상태 값을 조정할 수 있습니다. 또한 고 수준으로 작성된 HLSL과 저 수준으로 작성된 쉐이더를 연결해서 사용할 수 있습니다. 또한, 고정 기능, 저 수준, 고 수준 모두를 혼합해서 사용이 가능합니다.

다음은 간단하게 저 수준과 HLSL을 혼합한 예입니다.

 

<저 수준, HLSL을 혼합해서 사용한 예: ef01_basic3_hlsl+asm.zip>

 

 

1.2.2 Multi-pass

앞서 DX Effect는 하나의 Technique에 여러 Pass를 가질 수 있다고 했습니다. 이것을 Multi-pass라 합니다. Multi-pass를 사용하면 같은 렌더링 물체에 대해서 서로 다른 렌더링 환경을 가지고 연속해서 그리는 상황에 대해서 여러 편리한 점이 많습니다. 고정 기능 파이프라인에서 Multi-pass와 같은 내용을 구현하려면 코드가 길어지고, 길어진 길이만큼 관리하기가 점점 어려워집니다.

예를 들어 HLSL 기초의 Glow 효과는 렌더링 상태 값을 변경해 가면서 CW, CCW 방식으로 같은 물체를 두 번 렌더링으로 만든 효과인데 HLSL을 사용하더라도 상태 설정 등은 D3D 디바이스의 고정 기능 함수들을 사용했었습니다그런데 Effect를 사용하면 다음과 같이 Pass안에서 지정하고 Effect 객체를 사용하는 C/C++ Pass의 숫자를 확인해서 단순히 이들 Pass들을 렌더링 하는 방식이 구성되어 코드의 훨씬 간결하게 됩니다.

 

technique Tech0               // Effect Technique

{

        pass P0         // 모델 렌더링

        {

               CULLMODE       = NONE;

               VertexShader = compile vs_1_1 VtxPrc0();

               PixelShader  = compile ps_1_1 PxlPrc0();

        }

 

        pass P1        // Glow 렌더링

        {

               CULLMODE       = CW;

               VertexShader = compile vs_1_1 VtxPrc1();

 

// C++

void CShaderEx::Render()

        // 상수 연결

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

        // Technique 지정

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

 

        // Pass 개수 확인

        UINT    nPass=0;

        hr = m_pEft->Begin( &nPass, 0 );

 

        // Pass 수 만큼 같은 물체를 렌더링

        for(UINT n = 0; n < nPass; ++n)

        {

#if D3DX_SDK_VERSION >21

               hr = m_pEft->BeginPass( n );

#else

               hr = m_pEft->Pass( n );

#endif

 

               hr = m_pDev->DrawPrimitiveUP( D3DPT_TRIANGLESTRIP, …);

 

#if D3DX_SDK_VERSION >21

               hr = m_pEft->EndPass();

#endif

        }

 

        hr = m_pEft->End();

 

  

<법선 벡터를 이용한 효과: ef02_pass1_glow1.zip, ef02_pass1_glow2.zip, ef02_pass1_laser.zip>

 

이처럼 Multi-Pass의 사용은 코드를 간결하게 만들고 제어를 Effect에서 작성이 되어 게임의 기본 골격은 프로그래머가 만들고 효과에 대한 처리와 상태 값들은 쉐이더 언어를 알고 있는 기획자가 만들게 되어 전체적인 게임의 연출의 완성도가 높아지고 작업 또한 적절히 분배 되는 장점이 있습니다.

현재의 그래픽 카드들은 한번의 렌더링에 대한 속도가 모니터의 Hz보다 높게 나오는 것이 많습니다. 사람의 눈은 24~30 프레임 정도이기 때문에 고성능 그래픽 카드에서 같은 물체를 다른 환경에서 반복적으로 렌더링 해서 시각적 효과를 높이는 방법이 많이 사용되고 있습니다. 부드러운 그림자, Post Effect 등은 대표적으로 같은 물체를 반복해서 만드는 예이며 이들은 다음 과정에서 다시 살펴 보겠습니다.

간단하게 Multi-Pass를 사용해서 Post Effect의 하나인 Blur 효과를 구현해 보겠습니다. Blur 효과는 이후 과정에도 나오지만 인접한 픽셀에 가중치를 주고 이 가중치를 인접한 픽셀에 곱한 다음 전부 더해서 최종 색상을 정하는 방법입니다.

 

Blur 효과 최종 색상 =

 

전체 장면을 저장한 텍스처를 사용하기 때문에 정점의 형식은 RHW를 사용하거나 뷰 행렬과 투영 행렬을 사용하게 되어 정점 처리에 대한 쉐이더는 입력 값을 그대로 출력으로 사용하게 되어 다음과 같이 간단한 쉐이더로 작성합니다.

 

VertexShader VtxPrc = asm

{

        vs_1_1

        dcl_position v0

        dcl_color0   v1

        dcl_texcoord v2

        mov oPos, v0

        mov oD0, v1

        mov oT0, v2

};

 

생동감 있는 Blur 효과를 만들기 위해서 먼저 전체 장면을 저장한 텍스처를 그대로 화면에 그리는데 절반(1/2)으로 낮추어서 그립니다.

 

float4 PxlPrc0(float4 Dif : COLOR0, float2 Tx0 : TEXCOORD0) : COLOR

{

        float4  Out= tex2D(sampTex, Tx0);

        Out     *=0.5f;

        return Out;

}

 

다음으로 장면을 저장한 텍스처에 Blur 효과를 적용합니다. 가중치(weight: w) Gaussian 분포 함수(I * exp( - x*x/delta))를 사용하게 되어 쉐이더의 exp() 함수를 이용합니다.

 

// Blur 효과

float4 PxlPrc1(float4 Dif : COLOR0, float2 Tx0 : TEXCOORD0) : COLOR

{

        int i=0; int iMax=4; float4 Out= 0.0f;

 

        // 인접한 픽셀에 가중치를 주고 이들을 더함

        // 먼저 x 방향으로 Blur효과 적용

        for(i=-iMax; i<=iMax; ++i)

        {

               float2 Tx = Tx0;

               Tx.x    += (i *m_fDeviation)/1024.f;

               float4  d = tex2D(sampTex, Tx);

               // Gaussian 분포 함수 f(x) = I * exp( - x*x/delta)

               float   e = i*i;

               e = -e/16.0f;

               Out += d * 1.0f* exp( e );

        }

        // y 방향으로 Blur효과 적용

        for(i=-iMax; i<=iMax; ++i)

        {

               float2 Tx = Tx0;

               Tx.y    += (i *m_fDeviation)/1024.0f;

               float4  d = tex2D(sampTex, Tx);

               float   e = i*i;

               e = -e/16.0f;

               Out += d * 1.0f* exp( e );

        }

 

Technique에서 Blur 효과를 적용하는 Pass Alpha Blending을 활성화하고 Source Dest는 전부 ONE으로 설정합니다.

 

technique Tech

{

        pass P1

        {

               AlphablendEnable= TRUE;

               SRCBLEND       = ONE;

               DESTBLEND      = ONE;

               VertexShader = (VtxPrc);

               PixelShader  = compile ps_2_0 PxlPrc1();

 

<인접 픽셀을 이용한 Blur 효과: ef02_pass2_blur.zip>

 

ef02_pass2_blur.zip를 실행하면 시간에 대해서 Blur 효과가 동적으로 적용되는 것을 볼 수 있습니다.

 

 

1.3 2D Sprite

HLSL이나 Effect D3D에서도 고급 내용이기 때문에 처음에는 익숙하지 않을 수 있습니다. 3D 프로그램은 게임 제작 등을 통해서 개인의 실력이 발전하는데 프로젝트가 준비 되지 않고 공부 하는 과정이라면 Effect를 연습하기 위해 2D 게임에 대한 Sprite Effect로 만드는 것을 추천합니다.

DX 10이상은 쉐이더가 기본입니다. Embedded 환경에서 대부분 사용되는 OpenGL ES는 버전 2.0부터 GLSL를 사용하고 GLSL HLSL과 많이 비슷합니다. 따라서 2D 게임을 위한 Sprite Effect로 작성하는 것은 꼭 필요한 일이라 할 수 있습니다.

ID3DXSprite는 크기, 회전, 이동, 색상을 설정할 수 있는 Sprite입니다. 여기에 단색화를 추가한Sprite를 쉐이더와 Effect로 구성해 봅시다.

 

먼저 입력 값들을 다음과 같이 단색화, 색상 적용 값, 텍스처 샘플러를 지정합니다.

 

int     m_bMono;               // 단색화 사용

float4  m_Diff;                        // 색상 값

sampler smp0 : register(s0);  // 샘플러

 

만약 Sprite의 정점이 RHW로 구성되어 있다면 정점 처리 과정은 특별히 처리할 일이 없으므로 입력 값을 그대로 출력하도록 작성합니다.

 

VertexShader VtxProc = asm    // 정점 처리: 입력 값을 그대로 출력

{

        vs_1_1

        dcl_position  v0

        dcl_texcoord  v1

        mov oPos, v0

        mov oT0 , v1

};

 

픽셀 처리 과정의 기본은 최종 색상을 주어진 색상과 텍스처의 색상의 곱으로 결정하는 것입니다. 그런데 단색화의 요구가 있을 수 있으므로 이 경우에는 rgb는 외부에서 주어진 rgb으로 사용하고 알파는 텍스처의 알파 값을 사용합니다.

 

float4 PxlProc(float4 Pos0: POSITION

            ,  float2 Tex0: TEXCOORD0) : COLOR0

{

        float4  Out= tex2D(smp0, Tex0);

 

        Out *= m_Diff;

        if(0 != m_bMono)              // 단색

        {

               Out.a *= m_Diff.a;     // 알파는 텍스처의 알파 사용

               Out.r= m_Diff.r;       // 입력 값 색상 사용

               Out.g= m_Diff.g;

               Out.b= m_Diff.b;

        }

 

        return Out;

}

 

Technique에서 렌더링 상태 머신 값은 알파 블렌딩을 활성화 하고 Source Dest의 알파 값은 각각 SRCALPHA, INVSRCALPHA를 사용합니다. 또한 U, V 값이 [0,1] 범위에서만 유효하도록 Address Mode Clamp로 설정합니다.

 

technique Tech

{

        pass P0

        {

               CULLMODE               = NONE;

               ALPHABLENDENABLE       = TRUE;

               SRCBLEND               = SRCALPHA;

               DESTBLEND              = INVSRCALPHA;

               ADDRESSU[0]            = CLAMP;

               ADDRESSV[0]            = CLAMP;

               VertexShader = (VtxProc);

               PixelShader  = compile ps_1_1 PxlProc();

        }

};

 

게임에서 단색화가 필요한 경우는 주로 그림자를 그릴 때입니다. 또한 2D 게임은 확대/축소에서도 2D 특유의 느낌을 살리기 위해 픽셀의 필터링을 적용하지 않지 않는 경우가 많이 있습니다. 그림자를 그릴 때만 부드럽게 적용하도록 필터링을 설정합니다.

 

if(bMono)      // 단색일 경우 필터링 적용

{

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

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

        m_pDev->SetSamplerState(0,D3DSAMP_MIPFILTER, D3DTEXF_LINEAR);

}

else

{

        m_pDev->SetSamplerState(0,D3DSAMP_MAGFILTER, D3DTEXF_NONE);

        m_pDev->SetSamplerState(0,D3DSAMP_MINFILTER, D3DTEXF_NONE);

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

}

 

<쉐이더, Effect로 만든 2D Sprite: ef03_sprite1_soft.zip>

 

직소(Jigsaw) 게임은 특정한 패턴으로 원본 이미지를 잘라내어야 합니다. 이것을 그래픽에서 작업한다면 굉장한 노동력이 필요합니다. 그런데 알파 맵을 만든다면 수작업으로 이미지를 잘라내는 일들을 줄일 수가 있습니다. 이미지의 특정한 영역을 잘라내도록 알파 값을 가진 텍스처를 Alpha 맵으로 부르도록 합시다. 알파 맵을 Sprite에 적용하기 위해서 다음과 같이 샘플러를 하나 더 추가합니다.

 

int     m_bTx1;                              // 알파 맵 사용

int     m_bMono;                      // 단색화 사용

float4  m_Diff;                              // 외부 색상 값

sampler smp0 : register(s0);          // 원본 이미지용 샘플러

sampler smp1 : register(s1);          // 알파 맵 용 샘플러

 

알파 맵 또한 UV를 갖도록 정점 구조체를 수정합니다.

 

struct VtxRHWUV1

{

        VEC2    p;      FLOAT   z;      FLOAT   w;

        VEC2    t0;     // 원본 이미지 좌표

        VEC2    t1;     // 알파 맵 이미지 좌표

        enum {FVF = (D3DFVF_XYZRHW|D3DFVF_TEX2),};

};

 

정점 처리 과정은 이전의 Sprite처럼 입력 값을 그대로 출력 값으로 복사하도록 작성합니다.

 

VertexShader VtxProc = asm    // 정점 처리: 입력 값을 그대로 출력

        dcl_texcoord0 v1       // 원본 좌표

        dcl_texcoord1 v2       // 알파 맵 좌표

        mov oT0 , v1

        mov oT1 , v2

 

픽셀 처리과정은 알파 맵 적용, 단색화 두 개의 선택이 있습니다. 먼저 알파 맵 적용의 요구가 있으면 원본 텍스처와 알파 맵을 곱셈(Modulate)로 처리합니다.

 

float4 PxlProc(          float4 Pos0: POSITION

                , float2 Tex0: TEXCOORD0      // 원본 입력 좌표

                , float2 Tex1: TEXCOORD1      // 알파 맵 입력 좌표

               ):COLOR0

{

        float4  Out= 0;

        float4  t0 = tex2D(smp0, Tex0);

        float4  t1 = tex2D(smp1, Tex1);

 

        if(0 != m_bTx1)                        // 알파 맵 사용이 있으면 두 텍스처를 곱함

               Out = t0 * t1;

        else

               Out = t0;

 

        Out *= m_Diff;

        if(0 != m_bMono)              // 단색

        {

               Out.a *= m_Diff.a;     // 알파는 텍스처의 알파 사용

               Out.r= m_Diff.r;       // 입력 값 색상 사용

 

ef03_sprite2_hard를 실행하면 다음과 같이 알파 맵이 적용된 단색화 부분과 텍스처 이미지가 출력되는 것을 볼 수 있습니다.

 

<알파 맵이 적용된 Effect로 만든 2D Sprite: ef03_sprite2_hard.zip>

 

이처럼 2D Sprite Effect 만들면 사용의 요구에 따라서 능동적으로 대처할 수 있으므로 본격적으로 게임 제작에 돌입하기 전에 Sprite Effect로 만들어 보도록 하기 바랍니다.

 

 

2 Effect와 조명

쉐이더와 HLSL의 기본적인 내용을 알고 있다면 게임 프로그램의 3D 장면 연출에서 가장 중요하고 또한 가장 먼저 쉽게 적용할 수 있는 부분이 조명입니다. 조명은 같은 모델이라도 전혀 다른 느낌을 연출할 수 있음을 고정 기능 파이프라인과 저 수준 쉐이더, 그리고 HLSL에서 충분히 경험했습니다. D3D의 조명을 간단히 정리하면 분산 조명은 램버트 확산을 배경으로 만들어져 있고, Specular 조명은 퐁 반사에 기반을 두고 있습니다. 또한 이 둘은 고정 기능 파이프 라인에서 분산 조명은 구로 쉐이딩(Henri Gouraud)으로, 그리고 스페큘러 쉐이딩으로 불리어지고 있습니다.

3D 기초 시간에 고정 기능 파이프라인은 정점을 기준으로 모든 것을 처리하고 있어서 정점의 숫자가 적은 경우 원하는 조명의 효과를 보지 못한다고 했습니다. 이것을 해결하기 위해서 픽셀 처리 과정에서 조명에 대한 계산이 필요하다고 했습니다.

이 강의에서는 조명에 대한 모든 것을 완성한다는 의미에서 픽셀 쉐이더 기반의 조명 처리뿐만 아니라 조명에 관련된 Toon 쉐이딩을 다시 살펴보고, 마지막으로 NPR 중에서 tonal art의 하나인 Hatching을 살펴 보겠습니다.

 

 

2.1 분산 조명

이전 장에서 공부 했듯이 분산 조명은 램버트 확산에 기반을 둔 D3D의 분산 조명은 램버트 확산에 기초를 두고 있습니다. 램버트 확산은 반사되는 빛의 세기(Intensity)를 빛의 방향과 정점의 법선 벡터와의 내적을 통해서 구합니다. 이를 구하는 공식은 다음과 같습니다.

 

반사 밝기 = dot(정점의 법선 벡터 N, 빛의 방향 벡터 L)

 

이 공식을 그대로 적용하게 되면 최종 색상의 범위는 -1.0 ~ +1.0 가 되므로 적정한 명도를 만들기 위해 우리는 다음과 같은 공식을 사용했었습니다.

 

반사 밝기 = a * dot(N, L) + b 또는 (a + dot(N, L)) * b

 

이 공식을 다음과 같은 HLSL로 쉽게 바꾸는 것도 연습했었습니다.

 

float3x3       m_mtRot;       // 회전 행렬

float3         m_vcLgt;       // 빛의 방향 벡터

float3 N = mul(Nor, m_mtRot); // 법선 벡터의 회전 변환

float3 L = -m_vcLgt;           // 빛의 방향 벡터 반전

float4 D = (0.5f + dot(N, L)) * 0.6f; // Lambert 공식으로 밝기 설정

 

<분산 조명 밝기: ht11_lam1_basic.zip>

 

분산 조명의 밝기를 정점 처리에서 결정했는데 이것을 픽셀 처리 과정에서 계산해 보도록 합시다.

픽셀 처리과정에서 조명 효과를 계산하는 이유는 정점 처리과정은 조명 효과를 먼저 계산해서 정점의 색상에 반영을 하고 이것을 다시 래스터 과정에서 정점 사이의 색상을 구로 쉐이딩으로 보간하기 때문에 좀 더 자연스러운 조명 효과를 만들기 위해서 픽셀 처리과정에서 조명 효과를 처리하도록 하는 것입니다.

정점 처리 과정에서 정점의 법선 벡터를 회전 변환만 적용하고 변환한 법선 벡터를 픽셀 쉐이더로 전달합니다. 픽셀 쉐이더로 전달할 때 지정된 법선 벡터의 레지스터가 없으므로 텍스처 좌표 레지스터를 사용합니다. 이로 인해서 전달된 법선 벡터는 소속이 텍스처 좌표여서 GPU는 인접한 법선들과 다음과 같은 공식을 가지고 내부에서 선형 보간을 수행합니다.

 

픽셀 처리과정의 법선 벡터

(w 는 보간에 필요한 비중(Weight))

 

텍스처 레지스터를 사용할 때 0번부터는 원래의 텍스처 좌표로 주로 사용되기 때문에 인덱스 끝 번호 7부터 현재 이용되지 않는 텍스처 좌표 레지스터를 사용할 수 있도록 다음과 같이 출력 구조체를 작성 합니다.

 

struct SVsOut

{

        float4 Pos : POSITION; // 출력 위치(oPos)

        float2 Tx0 : TEXCOORD0;       // 출력 텍스처 좌표(oT0)

        float3 Nor : TEXCOORD7;       // 텍스처 좌표를 변환된 법선의 좌표로 사용

};

 

조명에 대한 정점 처리는 법선 벡터의 회전만 변환합니다.

 

SVsOut VtxPrc( float4 Pos : POSITION, // 입력 위치

               float4 Nor : NORMAL,   // 입력 법선 벡터

               float2 Tx0 : TEXCOORD0 // 입력 텍스처 좌표

        float3 N = mul(Nor, m_mtRot); // 정점의 법선 벡터의 회전 변환

        Out.Pos = P;           // 출력 위치 복사

        Out.Nor = N;           // 출력 법선 벡터

        Out.Tx0 = Tx0;         // 출력 텍스처 좌표

        return Out;

 

정점에서 처리했던 분산 조명을 픽셀 처리과정에서 연산합니다. 최종 색상은 물체의 텍스처와 혼합해서 출력하되 알파 값은 텍스처의 알파 값을 사용하도록 합니다.

 

float4 PxlPrc(SVsOut In) : COLOR

{

        float4 Out = 0;                              // 출력 색상

        float3 N = normalize(In.Nor);         // 법선 벡터의 정규화

        float3 L = -m_vcLgt;                  // 빛의 방향 벡터의 반전

 

        Out   = (0.5f + dot(N, L)) * 0.6;     // Lambert

        Out.a = 1.0f;                         // 텍스처의 알파를 사용하도록 1로 설정

        Out *= tex2D( SampDif, In.Tx0 );      // 텍스처의 색상과 곱셈

        return Out;

 

<분산 조명과 텍스처: ht11_lam2_diffuse.zip>

 

만약 여러분이 Effect를 사용하지 않고 분산 조명을 처리한다면 정점 쉐이더, 픽셀 쉐이더 객체를 생성하고 상수 값들을 설정하기 위해 상수 테이블을 사용해야 하겠지만 Effect의 사용으로 인해서 이들 상수 값들이 어느 처리에서 적용되는지 큰 고민 없이 편하게 작성할 수 있게 되었습니다

 

분산 조명에 대한 강의를 맺는 의미로 다중 조명 처리를 만들어 보도록 하겠습니다. 반사의 밝기와 빛의 색상을 적용하면 반사된 빛의 최종 색상은 다음과 같이 설정할 수 있습니다.

 

분산 조명 색상 =

 

이것을 쉐이더로 구현하면 for문을 통해서 분산 조명과 빛의 색상을 반복적으로 적용해서 최종 색상을 만들어 냅니다.

 

float3  m_LgtDir[4];   // 광원 방향 벡터 배열

float4  m_LgtDif[4];   // 광원 색상 배열

float4 Out = 0;                                      // 출력 색상

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

{

        float4 t=0;

        t = 0.25f + dot(N, -m_LgtDir[i]) * .8f;      // Lambert

        t *= m_LgtDif[i];

        Out +=t;

}

 

 

<여러 광원을 이용한 분산 조명 효과: ht11_lam3_multi.zip - 'T', 'R' >

 

여러 강원을 이용한 분산 조명 효과 예제나 그 이전의 분산 조명과 텍스처 예제는 픽셀 처리 과정에서 밝기를 계산했었습니다. 그런데 이들은 정점 처리 과정에서 계산한 것과 뚜렷한 차이를 느끼지 못할 수 있습니다. 원래 정점 사이의 분산 조명의 밝기를 정확하게 계산하려면 사원수의 보간에서 유도된 공식을 이용해서 다음과 같이 법선 벡터를 구해야 합니다.

 

, (의 사이 각, w=[0,1]의 비중 값)

 

그런데 두 법선 벡터의 사이 각이 작다면 , , 이 되어 법선 벡터는 좀 더 간단한 형태가 됩니다.

 

 

이것은 법선 벡터를 텍스처 좌표로 넘길 때 GPU가 계산한 법선 벡터와 같습니다. 이로 인해서 분산 조명을 정점 처리 과정에서 계산한 것과 픽셀 처리 과정에서 계산 결과가 크게 차이가 나지 않는 것입니다.

 

 

2.2 Specular 효과

구로 쉐이딩을 다시 살펴 보면 그림과 같이 3개의 정점으로 구성된 삼각형에서 p 위치의 색상은 정점 0과 정점 2로 보간한 I0 색상과 정점 1과 정점 2로 보간한 I1 색상을 다시 보간 해서 구해 집니다.

 

<정점 처리 과정의 구로 쉐이딩: 색상을 먼저 계산하고 이들을 보간>

 

이것은 분산 조명 효과에서는 거의 문제가 되지 않지만 Specular 조명 효과에서는 정점의 개수가 적으면 다음 그림과 같은 상태를 볼 수 있습니다.

 

 

<정점 쉐이더를 사용한 Specular 효과: ht12_spec1_vtx.zip>

 

이것은 정점의 Specular 효과를 점 p에 해당하는 법선 벡터와 시선 벡터로 구하지 않고 정점에서만 계산하고 p 점에서는 이 값을 선형 보간 하기 때문입니다.

그리고 정점 처리과정에서 계산되는 Specular 값은 Diffuse에 반영되고 Diffuse 색상 값은 래스터 처리 후에는 [0, 1]으로 정규화되어 있어서 1보다 큰 빛의 반사 값을 만들어 놓아도 픽셀 처리 과정에서 이 값을 사용할 수 없게 되어 강렬한 빛의 효과를 만들기가 어렵습니다.

픽셀 p의 위치에서 정확한 Specular 효과를 만들기 위해서는 I0에서 법선 벡터를 정점 0과 정점 1의 법선의 보간을 만들고 I1에서는 법선 벡터를 정점 1과 정점 2의 법선의 보간으로 만듭니다. 그리고 I0 I1의 법선을 가지고 픽셀 p의 위치에 맞는 법선 벡터를 구해서 이것을 Specular 공식에 적용해야 합니다.

 

 

<정점 법선 벡터를 텍스처 좌표로 픽셀 처리로 넘기기>

 

현재로서는 이렇게 정확하게 계산할 수 있는 GPU는 그리 많지 않습니다. 대신 픽셀 쉐이더 2.0 이상을 사용하면 어느 정도 이를 구현할 수 있는데 방법은 정점의 법선 벡터를 텍스처 좌표로 저장해서 픽셀 처리기로 넘기는 것입니다. 이렇게 하면 픽셀 처리기는 입력 받은 값은 법선 벡터 값이지만 텍스처 좌표이기 때문에 각각의 픽셀에 대해서 이 값을 선형 보간을 합니다.

선형 보간된 이 텍스처 좌표 값은 크기가 1이 아니므로 법선 벡터로 사용하기 위해서 꼭 정규화를 해야 합니다. 또한 시선 벡터도 텍스처 좌표로 픽셀 처리로 넘기고 픽셀 처리 과정에서 정규화해서 사용합니다.

지금까지의 내용을 구현해 보도록 하기 위해서 먼저 정점 처리과정에서 Specular 효과에 대한 Phong 반사를 작성해 봅시다. 이전에 저 수준, HLSL로 해봤기 때문에 지금은 어려움이 없습니다.

 

float3x3       m_mtRot;       // 법선 벡터

float3         m_vcLgt;       // 빛의 방향 벡터

float3         m_vcCam;       // 카메라 위치

float          m_fShrp;       // Sharpness

 

// 정점 처리 프로세스

SVsOut VtxPrc( float3 Pos : POSITION, // 입력 위치

               float3 Nor : NORMAL    // 입력 법선 벡터

        // 시선 벡터 = 정규화(카메라 위치 정점 위치)

        float3 E = normalize(m_vcCam - P);

 

        float3 N = normalize(mul(Nor, m_mtRot));     // 법선 벡터의 회전

        float3 L = normalize(-m_vcLgt);                      // 빛의 방향 반전

        float3 R = normalize( 2 * dot(N, L) * N - L); // 반사 벡터

        float4 S = pow( max(0, dot(R, E)), m_fShrp); // Phong 반사 세기

 

<ht12_spec1_vtx.zip>

 

다음으로 정점 처리에서 구현한 Specular를 픽셀 처리에서 구현하기 위해서 법선 벡터와 시선 벡터를 픽셀 처리기가 보간할 수 있도록 다음과 같이 정점 처리의 출력에 대한 구조체에 텍스처 좌표 Semantic을 사용해서 정점의 법선 벡터와 시선 벡터를 저장할 수 있도록 구성합니다.

 

struct SVsOut

{

        float4 Pos : POSITION;        // 출력 위치

        float3 Nor : TEXCOORD6;       // 변환된 법선 벡터를 텍스처 좌표 6에 저장

        float3 Eye : TEXCOORD7;       // 시선 벡터를 텍스처 좌표 7에 저장

};

 

정점 처리 과정에서는 단순하게 법선 벡터를 회전 변환 하고, 시선 벡터를 구해서 출력 구조체 변수에 저장합니다.

 

SVsOut VtxPrc(…)

        // 시선 벡터 = 정규화(카메라 위치 정점 위치)

        float3 E = normalize(m_vcCam - P);

        float3 N = mul(Nor, m_mtRot); // 법선 벡터의 회전

        Out.Eye = E;   // 시선 벡터 복사

        Out.Nor = N;   // 변환된 법선 벡터 복사

        return Out;

 

픽셀 처리 과정에서 먼저 입력 받은 정점의 시선 벡터, 법선 벡터를 다시 정규화 합니다. 이것은 이들 값들이 텍스처 좌표로 넘어와서 픽셀 처리기가 보간하기 때문에 단위 벡터인 크기를 다시 1로 조정하기 위해서 입니다. 그리고 픽셀 쉐이더 버전은 반드시 2.0 이상으로 설정해야 제대로 동작합니다.

 

// 픽셀 처리 프로세스: 반드시 2.0 이상 버전 필요

float4 PxlPrc(SVsOut In) : COLOR

        float3 E = normalize(In.Eye); // 시선 벡터 정규화

        float3 N = normalize(In.Nor); // 법선 벡터 정규화

        float3 L = normalize(-m_vcLgt);

        float3 R = reflect(-L, N);    // 반사 벡터: 내장 함수 reflect() 이용. 부호 주의

        float4 S = pow( max(0, dot(R, E)), m_fShrp); // 퐁 반사

 

전체 코드는 ht12_spec2_pxl.zip에 구현 되어 있으며 실행하면 다음과 같은 장면을 볼 수 있습니다.

 

 

<픽셀 쉐이더 2.0 이상을 사용한 Specular 효과: ht12_spec1_pxl.zip>

 

분산 조명은 텍스처의 색상과 곱셈 연산을 했지만 Specular 효과는 빛의 고 휘도 부분을 표현한 것이므로 곱셈 연산 대신 덧셈을 합니다. 이것을 HLSL로 다음과 같이 구현합니다.

 

texture m_TxDif;

sampler SampDif = sampler_state

{

    texture = (m_TxDif);

float4 PxlPrc(SVsOut In) : COLOR

{

        float4 Out;                                  // 출력 색상

        float4 S = pow( max(0, dot(R, E)), m_fShrp); // 퐁 반사

 

        Out = tex2D( SampDif, In.Tex );                       // 텍스처 색상 추출

        Out +=S;                                      // 퐁 반사 값과 더함

        return Out;

 

<퐁 반사와 텍스처 혼합: ht12_spec3_tex.zip>

 

D3D의 조명 효과를 정리하는 의미로 Specular 효과와 분산 조명에 대한 다중 조명을 구현해 봅시다. 먼저 퐁 반사로 구현된 Specular 색상을 수식으로 표현해 봅시다.

 

Specular 색상 =

 

만약 Blinn-Phong 반사라면 시선 벡터 E와 빛의 반사 벡터 R 대신 Half 벡터 H와 법선 벡터 N을 사용합니다. 이 수식에 이전의 분산 조명 수식을 더하면 조명 효과에 대한 Specular 반사가 완성됩니다.

 

최종 색상 =

 

그런데 텍스처가 적용되면 분산 조명은 텍스처 색상과 곱셈을 하고 Specular를 더하는 방법을 가장 많이 사용합니다.

 

최종 색상 =

              

 

조금 긴 수식이지만 구현하는데 어려움이 없습니다.

 

float   m_fShrp;               // Sharpness

float3  m_LgtDir[4];           // 조명 방향 벡터 배열

float4  m_LgtDif[4];           // 조명 색상 배열

float4 PxlPrc(SVsOut In) : COLOR0

float3 N = normalize(In.Nor); // 법선 벡터 정규화

float3 E = normalize(In.Eye); // 시선 벡터 정규화

 

float3 L[3]; float3 R[3]; float4 Lam =0; float4 Spc =0;

 

for(i=0; i<3; ++i)            // 반사 벡터 계산

{

        L[i] = normalize(-m_LgtDir[i]);

        R[i] = reflect(-L[i],N);

}

 

for(i=0; i<3; ++i)            // 분산 조명: Σdot(N, L_i) * Color_i

        Lam += (0.5f * dot(N, L[i]) + 0.5)*m_LgtDif[i];

 

 

for(i=0; i<3; ++i)            // 퐁 반사: Σdot(E, R_i)^Sharpness * Color_i

        Spc += pow( max(0, dot(E, R[i])), m_fShrp)*m_LgtDif[i];

 

Out = saturate(Lam);          // 색상의 범위 [0,1]로 제한

 

if(1 == m_bTexture)           // 분산 조명 값과 텍스처 혼합

        Out *= tex2D( SampDif, In.Tex );

 

Out += Spc;                   // Specular 값을 더함

 

 

<다중 Specular와 분산 조명: ht12_spec4_multi.zip - R, T >

 

for 문에서 3개의 조명만 사용한 것은 ps_2_0의 명령 슬롯이 적기 때문이며 ps_3_0이상 지원이 되는 GPU는 보다 많은 명령 슬롯을 가지고 있어서 이것을 가지고 있는 분들은 조명의 숫자를 좀 더 늘려서 연습하기 바랍니다.

 

 

2.3 조명과 NPR

2.3.1 Cartoon Shading

우리는 저 수준 쉐이더 강의에서 툰(Toon: Cartoon) 쉐이딩 방법을 구현해 보았습니다. 툰 쉐이딩을 간단히 정리하면 연속적인 반사의 밝기를 이산적인(Discrete) 밝기로 만드는 것입니다. 이를 구현하기 위해서 이산적인 값은 불연속적인 텍스처의 흑백 색상으로 만들고 조명의 밝기를 이 텍스처의 좌표로 만들어 샘플링을 통해서 불연속 텍스처의 색상을 가져오도록 하면 툰 쉐이딩 구현이 완료 됩니다.

 

저 수준에서 해보았던 툰 쉐이딩을 HLSL로 만들어 봅시다. 먼저 정점 출력 구조체에 조명의 밝기를 저장할 수 있도록 1차원 텍스처 좌표를 Semantic으로 하는 구조체를 구성합니다.

 

struct SVsOut

{

        float4 Pos : POSITION;        // 출력 위치

        float  Toon: TEXCOORD7;               // 1차원 Toon Texture 좌표

};

 

정점 처리에서는 Lambert 공식으로 조명의 반사 세기(Intensity)를 계산하고 이 반사 값을 1차원 텍스처 좌표에 저장합니다.

 

float3x3       m_mtRot;       // 회전 행렬

float3         m_vcLgt;       // 조명의 방향 벡터

SVsOut VtxPrc( …, float3 Nor : NORMAL                       // 입력 법선 벡터

        float3 N = normalize(mul(Nor, m_mtRot));     // 법선 벡터의 회전

        float3 L = normalize(-m_vcLgt);

        float4 D = 0.4 + 0.6 * dot(N, L);            // 조명의 밝기 계산

        // [0, 1]로 제한된 밝기를 1차원 텍스처 좌표로 저장

        Out.Toon = saturate(D);

        return Out;

 

툰 효과를 만들기 위해서 다음과 같은 텍스처를 생성하고 이것을 샘플링 할 수 있도록 샘플러를 만듭니다.

 

<카툰(Cartoon) 텍스처>

 

texture m_TxToon;                     // Toon 텍스처

sampler SampToon = sampler_state      // 샘플러

{

        texture = (m_TxToon);

};

 

정점 처리 과정에서 만들어진 1차원 텍스처 좌표를 픽셀 처리 과정에서 샘플링하고 외부에서 주어진 색상과 곱하면 툰 쉐이딩이 완성 됩니다.

 

float4  m_ToonColor;   // Toon 색상

float4 PxlPrc(SVsOut In) : COLOR0

{

        float4 Out;

        Out = tex2D( SampToon, In.Toon ) * m_ToonColor;

        return Out;

}

 

<Toon Shading: ht13_toon1_basic.zip>

 

툰 쉐이딩에 종종 외곽선을 적용하기도 합니다. 외곽선을 적용하는 가장 쉬운 방법은 법선 벡터를 정점의 위치에 더하고 이것을 CW로 그리는 것입니다. 이 방법은 Glow 효과 때도 구현해 보았으므로 이것을 그대로 사용하면 됩니다.

 

// 외곽선용 정점 처리 프로세스

SVsOut VtxPrc0(float4 Pos : POSITION0 // 입력 정점 위치

             , float4 Nor : NORMAL0   // 정점 법선 벡터

        float4 P = Pos;

        float4 N = Nor;

 

        P +=N *.05F;          // 정점의 위치에 법선 벡터를 더한다.

        P.w = 1.0f;            // w=1로 한다.

        P = mul(P, m_mtWld);   // 월드 변환

        Out.Pos = P;           // 출력 위치에 복사

 

픽셀 처리 함수는 외곽선 처리는 외부에서 주어진 색상으로 단색을 만들고 툰 쉐이딩은 툰 텍스처에서 샘플링 할 수 있도록 합니다.

 

float4 PxlPrc(SVsOut In, uniform int bTexture) : COLOR0

        if(0==bTexture)               // 외곽선 처리

               Out = In.Dff;

        else                   // 툰 효과 처리

               Out = tex2D( SampToon, In.Toon );

 

        return Out;

 

technique에서 외곽선을 CW로 먼저 그릴 수 있도록 하기 위해서 pass 0에 정점 쉐이더와 픽셀 쉐이더 객체를 구성 합니다. pass 1에서는 툰 쉐이딩이 적용되도록 CCWCull Mode를 구성합니다.

 

technique Tech0

{

        pass P0

        {

               CULLMODE     = CW;

               ZWRITEENABLE = FALSE;

               VertexShader = compile vs_2_0 VtxPrc0();

               PixelShader  = compile ps_2_0 PxlPrc(0);

        }

        pass P1

        {

               CULLMODE     = CCW;

               ZWRITEENABLE = TRUE;

               VertexShader = compile vs_2_0 VtxPrc1();

               PixelShader  = compile ps_2_0 PxlPrc(1);

        }

}

 

<툰 쉐이딩 + 외곽선: ht13_toon2_edge.zip>

 

카툰 효과는 분산 조명에 대한 이산 효과라 한다면 Diffuse 텍스처와 함께 렌더링 물체에 적용할 때 분산 조명과 마찬가지로 이들을 곱해야 합니다. 그런데 단순하게 곱해 버리면 카툰 효과를 충분히 발휘 할 수 없을 수도 있습니다.

카툰 효과는 주어진 툰 텍스처 보다 밝기(Intensity)뿐만 아니라 Contrast를 높여야 할 때도 있습니다. 밝기를 올리려면 툰 효과에 사용되는 밝고 어두운 텍스처에 적절한 값을 곱하고 Contrast는 쉐이더의 pow() 함수를 사용합니다.

 

float4 PxlPrc(SVsOut In) : COLOR

        Out = tex2D(SampToon, In.Toon)*1.2; // 적당한 값을 곱해서 밝기를 올림

        Out = pow(Out, 3)/1.2;            // pow() 함수 이용, contrast를 높임

        Out *= tex2D( SampDif, In.Tx0 );    // Diffuse 텍스처와 혼합

        Out.a = 1;

        return Out;

 

 

<+텍스처: ht13_toon3_diffuse.zip>

 

 

2.3.2 Hatching

3D의 조명의 원리를 이해하고 있다면 툰 쉐이딩을 쉽게 구현 할 수 있음을 우리는 잘 알 수 있습니다. 툰 쉐이딩 이외에 조명 원리를 간단하게 적용해서 구현해 볼 수 있는 것이 Hatching 입니다.

ht13_hatching_tonal_art1.zip 예제를 실행하면 반사의 세기에 따라서 적당한 Hatching으로 만들어진 텍스처가 적용되고 있음을 볼 수 있습니다.

 

<NPR-tonal art map. ht13_hatching_tonal_art1.zip>

 

이 기술은 의외로 간단합니다. 구현 방법은 반사의 밝기를 일정한 구간으로 나누어서 구간에 해당하는 2개의 텍스처를 적절하게 혼합하는 것입니다. 예를 들어서 밝기에 대한 텍스처 A, B, C, D, E 5장이 준비되어 있다면 [0., 0.25) 범위는 A B 텍스처를, [0.25, 0.5) 구간에 대해서는 B, C, [0.5, 0.75) C, D, 그리고 [0.75, 1.0) 범위는 D, E 텍스처를 사용해서 혼합하는 것입니다.

그런데 이렇게 전체 밝기를 [0, 1] 사이로 하면 혼합하는 텍스처의 비중(Weight)를 구하는 것이 혼잡하기 때문에 각 구간의 크기를 1로 정하고, 반사의 밝기를 텍스처 개수(n-1) 만큼 곱합니다. 이렇게 하면 각각의 구간은 [0., 1.), [1.0, 2.), …, [n-2, n-1), [n-1, )이 만들어 집니다.

만약 밝기가 0.7이고 전체 텍스처가 A(0), B(1), C(2), D(3), E(4) 5 장이면 0.7 * (5-1) = 2.8로 계산을 하고 [2., 3.) 구간의 텍스처 C D에 비중 (2.8-2) (3 - 2.8)을 각각 곱하고 곱한 결과를 더하면 최종 출력 색상이 됩니다.

 

간단한 원리 이기 때문에 곧바로 이것을 쉐이더로 구현해 봅시다. 만약 다음과 같이 밝기에 대한 6장의 텍스처가 준비되어 있다면 이들에 대한 샘플러 또한 6개를 선언해야 합니다.

 

sampler smp0 : register(s0) = sampler_state{…};

sampler smp0 : register(s1) = sampler_state{…};

sampler smp0 : register(s2) = sampler_state{…};

sampler smp0 : register(s3) = sampler_state{…};

sampler smp0 : register(s4) = sampler_state{…};

sampler smp0 : register(s5) = sampler_state{…};

 

  

  

<밝기에 대한 Hatching Texture>

 

반사 모델은 Lambert 확산을 적용한다면 정점 처리 과정에서 반사의 세기를 계산해도 충분합니다.

 

struct SVsOut

{

        float4 Pos : POSITION;

        float4 Dif : COLOR0;          // 분산 조명 밝기

        float2 Tex : TEXCOORD0;               // 디퓨즈 맵 좌표

};

float3x3 m_mtRot;                      // 회전 행렬

float3  m_vcLgt;                      // 조명의 방향 벡터

 

SVsOut VtxPrc(…, float4 Nor : NORMAL0)

{

        SVsOut Out = (SVsOut)0;

        float3 N = normalize(mul(Nor,m_mtRot));      // 법선 벡터의 회전

        float3 L = normalize(m_vcLgt);

        float4 D = (1.f+dot(N, L)) * 0.5f;    // Lambert 확산으로 밝기 설정

        Out.Dif = D;                          // 출력 구조체에 복사

 

픽셀 처리 과정의 최종 색상은 각 구간에 맞게 비중(Weight)을 구하고 해당 텍스처에 곱한 다음 더하면 됩니다.

 

float4 PxlPrc(SVsOut In) : COLOR0

        float   htcLvl = In.Dif * 5.3; // 밝기를 5보다 약간 큰 5.3배를 한다

 

        // Hatching 비중 값을 전부 0으로 설정

        float   Htch0 = 0;

        float   Htch1 = 0;

        float   Htch2 = 0;

        float   Htch3 = 0;

        float   Htch4 = 0;

        float   Htch5 = 0;

 

        // 밝기에 대한 구간을 찾고 비중을 계산

        if(htcLvl > 5.0)

        {

               Htch0 = 1.0;

        }

        else if(htcLvl > 2.0)

        {

               Htch2 = 1.0 - (3.0 - htcLvl);

               Htch3 = 1.0 - Htch2;

        }

        else if(htcLvl > 1.0)

        {

               Htch3 = 1.0 - (2.0 - htcLvl);

               Htch4 = 1.0 - Htch3;

        }

        // 텍스처 샘플링 값에 해당 텍스처의 비중을 곱함

        float4 t0 = tex2D(smp0, In.Tex*m_fHtchW) * Htch0;

        float4 t1 = tex2D(smp1, In.Tex*m_fHtchW) * Htch1;

        float4 t2 = tex2D(smp2, In.Tex*m_fHtchW) * Htch2;

        float4 t3 = tex2D(smp3, In.Tex*m_fHtchW) * Htch3;

        float4 t4 = tex2D(smp4, In.Tex*m_fHtchW) * Htch4;

        float4 t5 = tex2D(smp5, In.Tex*m_fHtchW) * Htch5;

 

        // 색상을 전부 더하고 출력

        Out  = t0 + t1 + t2 + t3 + t4 + t5;

        Out.a = 1.0;

        return Out;

 

만약 Diffuse 텍스처를 적용하려면 다음과 같이 마지막 단계에서 Diffuse 텍스처를 샘플링하고 출력 색상에 곱하면 됩니다.

 

        Out = t0 + t1 + … + t5;

        Out.a = 1.0;

        Out = pow(Out, 0.6)*1.5;              // pow()함수로 Contrast를 낮춤

        Out *= tex2D( SampDif, In.Tex );      // Multiply Diffuse Texture

        return Out;

 

<Diffuse 텍스처가 적용된 NPR-tonal art map. ht13_hatching_tonal_art2.zip>

 

 

3 Mapping

어떤 오래된 책의 번역을 보면 텍스처를 질감으로 번역해 놓은 경우도 있습니다. 이론적으로 정점만 있어도 모든 가상의 물체를 표현할 수 있지만 현실과 같은 효과를 주기 위해서는 수백만 개 이상의 정점이 필요할지도 모릅니다. 텍스처는 화면에 동시에 연출 되는 정점 수에 대한 메모리를 줄이고 3차원 표면을 사실처럼 묘사하기 위해 2차원 또는 3 차원 이상의 픽셀로 구성되어 있는 것을 3D 기초와 지금까지의 강의 내용으로 잘 알고 있습니다.

텍스처를 정점에 적용하는 것을 매핑이라 하는데 매핑은 단순하게 정점에 2차원(혹은 1차원, 3차원) 좌표를 설정하는 것으로 그래픽 카드는 이것을 픽셀 처리 과정에서 해당 좌표에서 적당한 샘플링 방법으로 색을 추출한 다음 정점의 색상과 혼합해서 최종 색상을 만들어 냅니다.

매핑은 그 목적과 구현 방법에 따라서 여러 종류가 있고 게임에서는 Diffuse Mapping, Lighting Mapping, Bump Mapping, Specular Mapping, Parallax Mapping, Environment Mapping, Shadow Mapping, Displacement Mapping 등이 있습니다. 또한 이들 매핑에서 적용되는 텍스처들은 00 Map이라 부릅니다. 예를 들어서 디퓨즈 매핑에 사용되는 텍스처를 디퓨즈 맵이라 하고, 라이팅 매핑에 사용되는 텍스처를 라이팅 맵, 범프 매핑에 적용되는 텍스처를 법선 맵(Normal Map) 또는 범프 맵(Bump Map), 그리고 스페큘러 매핑에 사용되는 텍스처를 스페큘러 맵이라 합니다.

이 강의에서는 게임에서 적용되는 각각의 매핑 기술의 원리와 구현을 살펴보겠습니다.

 

 

3.1 Lighting Mapping

디퓨즈 매핑(Diffuse Mapping)은 우리가 가장 기초적으로 사용하는 매핑 기술로 정점에 단순히 색상을 적용하는 것으로 물체의 표면에 반사되는 난반사에 대한 분산광 효과를 표현하기 때문에 디퓨즈 매핑이라 합니다. 라이팅 매핑(Lighting Mapping)은 그림자 또는 조명의 밝은 부분 표현하기 위해서 밝기(음영)에 대한 값을 텍스처(라이팅 맵)로 만들고 디퓨즈 맵과 Multi-Texturing으로 처리한 픽셀 처리에서 혼합 다중 텍스처 처리로 처리합니다.

3D 게임 초창기 그래픽 카드는 화면에 연출되는 물체의 정점 수가 그리 많지 않았을 때 광원에 대한 효과도 제대로 살리기 어려웠는데 이것을 텍스처로 해결했습니다. , 정점에 텍스처에 빛의 광원 효과를 저장해서 장면 연출을 할 때 디퓨즈 맵과 다중 텍스처 처리를 합니다.

라이팅 매핑은 부드러운 조명 효과를 적절하게 만들어 주고 표현 또한 우수해서 ID 회사의 게임 '3D 퀘이크'에서 첫 선을 보이면서 현재까지 광범위하게 사용되고 있는 기술이며 아직까지 대부분의 3D 엔진이나 지형 툴에서 라이팅 맵 설정을 지원하고 있으며 실제 게임에서도 많이 응용되고 있습니다.

현재 일부 엔진의 경우 장면의 렌더링 속도를 위해서 지형의 경우 라이팅 맵과 디퓨즈 맵을 통합해서 하나의 디퓨즈 맵으로 가져 가는 경우도 있습니다.

 

라이팅 매핑은 다음과 같이 두 장의 텍스처를 Modulate 연산으로 처리합니다.

최종 색상 = Diffuse Map * Lighting Map * 밝기 상수

 

이 연산 방법은 3D 기초 시간의 다중 텍스처 처리(Multi-Texturing)에서 연습한 내용이고 구현하기가 너무나 간단해서 쉐이더를 사용하지 않고 고정 기능 파이프라인에서도 얼마든지 처리할 수 있습니다.

고정 기능 파이프 라인에서 이것을 처리할 때도 앞서 배운 Effect Technique에서 상태 설정을 하게 되면 좀 더 간결해지고 쉬어집니다.

 

// 디퓨즈 맵

texture m_TxDif;

sampler SampDif = sampler_state { texture = <m_TxDif>; … };

 

// 라이팅 맵

texture m_TxLgt;

sampler SampLgt = sampler_state { texture = <m_TxLgt>; … };

 

technique Tech0

pass P1

{

        sampler  [0] = (SampDif);

        sampler  [1] = (SampLgt);

 

        COLORARG1[0] = TEXTURE;

        COLORARG2[0] = DIFFUSE;

        COLOROP  [0] = SELECTARG1;

        ALPHAOP  [0] = DISABLE;

        ALPHAARG1[0] = TEXTURE;

        ALPHAOP  [0] = SELECTARG1;

        TEXCOORDINDEX[1] = 0;          // 0 단계 텍스처 좌표 사용

        COLORARG1[1] = TEXTURE;

        COLORARG2[1] = CURRENT;

        COLOROP  [1] = MODULATE2X;    // 색상 연산

        ALPHAARG1[1] = TEXTURE;

        ALPHAARG2[1] = CURRENT;

        ALPHAOP  [1] = MODULATE;

 

        COLOROP  [2] = DISABLE;

        ALPHAOP  [2] = DISABLE;

 

다중 텍스처 처리 0 단계에서 색상의 입력 값 1((Color Argument1: COLORARG1[0])을 텍스처로 설정하고 정점의 색상 값을 입력 값 2((Color Argument1: COLORARG2[0])으로 설정합니다. 색상 혼합은 텍스처를 선택합니다. 텍스처의 색상과 정점의 색상을 혼합하는 OP 값을 MODULATE하는 것이 보통이지만 현재는 순전히 텍스처의 색상만 적용하므로 OP 값을 stage 입력의 1번을 선택 했습니다. 알파는 텍스처의 알파를 그대로 사용합니다.

다중 텍스처 처리 1 단계에서는 0 단계에서 출력한 색상 Current를 가지고 라이팅 맵과 혼합하는 단계입니다. 입력 색상 1(COLORARG1[1])은 라이팅 맵의 텍스처를 선택하고, 0 단계의 출력 값 CURRENT COLORARG2로 지정해서 이 둘을 곱(MODULATE) 하도록 합니다. 만약 색상에 대한 OP MODULATE로 선택했을 때 어둡게 나오면 MODULATE2X MODULATE4X를 선택합니다.

알파 값은 두 텍스처의 곱셈인 MODULATE를 선택합니다. 다음 단계에서 다중 텍스처 처리를 안 하도록 COLOROP[2] ALPHAOP[2] DISABLE로 합니다.

다음의 ht21_lightmap_fixed.zip 예제는 색상의 혼합을 MODULATE2X MODULATE4X를 사용했습니다.

 

 

<고정 기능 파이프라인의 라이팅 매핑. ht21_lightmap_fixed.zip>

 

고정 파이프라인에서 라이팅 맵을 사용하기 위해서 다중 텍스처 처리를 사용하기 위해서 각 단계에 대한 상태 값을 설정했지만 쉐이더를 사용하면 라이팅 맵과 디퓨즈 맵은 더하기, 빼기, 곱하기, 나누기 등의 적당한 산술 식으로 라이팅 매핑을 다음과 구현 할 수 있습니다.

 

// 라이팅 매핑 픽셀 처리 함수

float4 PxlPrc(SVsOut In, uniform int nIntensity) : COLOR

{

        float4 Out;

        float4 D = tex2D( SampDif, In.Tx0 );         // Diffuse Map

        float4 L = tex2D( SampLgt, In.Tx0 );         // Lighting Map

 

        Out = D;

        if(0!=nIntensity)

               Out *= L * nIntensity;

 

        return Out;

}

 

HLSL을 사용하면 좀 더 공식에 맞는 코드를 구현할 수 있습니다. 또한 내장 함수 pow() 등을 사용해 Contrast도 적절하게 만들어 낼 수 있습니다.

ht21_lightmap_shader.zip은 라이팅 매핑의 공식을 그대로 사용한 예제로 전체 밝기를 x1, x3, x6 을 구현했습니다.

 

 

<Programmable Pipeline Lighting Mapping. ht21_lightmap_shader.zip>

 

 

3.2 Bump Mapping (Normal Mapping)

범프 매핑(Bump Mapping)은 적은 수의 정점을 가지고 보다 높은 광원 효과를 만들기 위해서 법선 벡터를 텍스처에 저장한 법선 맵(Normal Map)을 픽셀 처리에서 픽셀 단위로 조명에 대한 반사 효과를 적용하는 방법입니다.

블린(Jim Blinn)은 정점의 숫자가 적은 단조로운 물체에 텍스처를 이용해서 밝고 어둠에 대한 음영을 더 사실감 있는 효과를 내기 위해서 반사의 세기를 결정하기 위한 법선 벡터를 이미지에 저장하고 프로그램에서 이 이미지의 픽셀을 법선 벡터로 변환해서 분산 조명, Specular 조명을 계산하도록 했습니다.

노말 맵을 이용한 반사 효과는 정점의 개수에 거의 독립적이며 올록볼록한 Embossing 효과를 만들기 때문에 범프 매핑(Bump Mapping)이라 부르게 되었습니다.

 

정점의 법선 벡터를 이미지에 저장하기 위해서 그래픽 담당자들은 렌더링에 필요한 정점 수보다 훨씬 많은 고 폴리곤(High Polygon)으로 메쉬 작업을 먼저 합니다. 그 다음으로 그래픽 툴을 이용해서 법선 벡터를 이미지에 저장을 하고 다시 이 이미지를 재 작업을 합니다. 프로그래머는 간단하게 구현할 수 있지만 그래픽을 만드는 분들에게는 상당히 노동력이 많이 드는 일이 아닐 수 없습니다.

정점의 법선 벡터 대신에 법선 벡터를 저장한 노말 맵(Normal Map)에서 벡터를 가져오는 일만 다를 뿐 반사의 밝기를 정하는 기본 처리는 분산 조명 또는 Specular 조명에서 사용한 공식 그대로 적용이 됩니다.

 

D3D의 고정 기능 파이프라인은 노말 맵에서 법선 벡터를 추출하고 이 법선 벡터와 빛의 방향 벡터를 내적(dot product) 연산 방법을 제공하고 있습니다. 구현 방법은 먼저 [-1, 1] 범위의 빛의 방향 벡터를 [0, 255] 색상 범위 값으로 만들고 32 비트 DWORD형으로 Tfactor에 저장합니다. 그 다음으로 다중 텍스처 처리에서 색상 혼합 COLOOROPDOTPRODUCT3로 합니다. DOTPRODUCT3 Tfactor와 텍스처의 색상을 내적으로 흑백의 명암을 만들어 냅니다.

프로그램의 구현은 먼저 빛의 방향 벡터를 [0, 255] 범위의 DWORD형으로 만드는 함수부터 작성합니다.

 

DWORD VectorToRGB(D3DXVECTOR3* vcNor)

{

        DWORD dwR = (DWORD)(127 * vcNor->x + 128);

        DWORD dwG = (DWORD)(127 * vcNor->y + 128);

        DWORD dwB = (DWORD)(127 * vcNor->z + 128);

        return (DWORD)(0xff000000 + (dwR << 16) + (dwG << 8) + dwB);

}

 

렌더링 머신의 상태 설정에서 빛의 방향 벡터가 DWORD형으로 변환된 값을 Tfactor에 저장합니다.

 

TEXTUREFACTOR= (m_dTFactor);

 

Color Arg1 또는 Arg2 둘 중 하나를 Tfactor로 지정하고, 노말 맵 텍스처는 나머지 Argument에 설정합니다.

 

COLORARG1[0] = TFACTOR;       // Tfactor 값을 ColorArg1으로 설정

COLORARG2[0] = TEXTURE;

 

Color Op DOTPRODUCT3로 설정하면 렌더링 머신이 Tfactor와 텍스처의 픽셀을 내적(dot product) 연산을 합니다.

 

COLOROP  [0] = DOTPRODUCT3;   // Tfactor에 저장된 값과 텍스처의 픽셀을 내적

 

 

디퓨즈 맵과 혼합하려면 여러 단계의 다중 텍스처 처리가 필요합니다. 0 단계에서 디퓨즈 맵의 색상을 가져와 CURRENT에 저장합니다. 1 단계는 Tfactor와 법선 맵의 색상을 내적을 하고 이 값을 TEMP에 저장합니다. 2 단계는 CURRENT에 저장된 값과 TEMP에 저장된 값을 혼합합니다.

 

sampler  [0] = (SampDif);     // 디퓨즈 맵

sampler  [1] = (SampNor);     // 법선 맵

 

TEXTUREFACTOR= (m_dTFactor);  // Tfactor

 

// 디퓨즈 맵의 색상 가져와서 CURRENT에 저장

COLORARG1[0] = TEXTURE;

COLOROP  [0] = SELECTARG1;

RESULTARG[0] = CURRENT;

// Tfactor와 법선 맵의 색상을 내적 하고 결과를 TEMP에 저장

COLORARG1[1] = TFACTOR;

COLORARG2[1] = TEXTURE;

COLOROP  [1] = DOTPRODUCT3;

RESULTARG[1] = TEMP;

// Current Temp에 저장된 색상을 혼합

COLORARG1[2] = CURRENT;

COLORARG2[2] = TEMP;

COLOROP  [2] = MODULATE;

RESULTARG[2] = CURRENT;

 

이 방법은 처리에 대해서 총 3 단계가 필요합니다. 그런데 Tfactor와 법선 맵의 연산을 먼저 진행하면 단계를 총 2 단계로 줄일 수 있습니다.

 

// 0 단계에서 Tfactor와 법선 맵을 dot 연산하고 결과를 CURRENT에 저장

COLORARG1[0] = TFACTOR;

COLORARG2[0] = TEXTURE;

COLOROP  [0] = DOTPRODUCT3;

ALPHAOP  [0] = DISABLE;

RESULTARG[0] = CURRENT;

// CURRENT의 저장되어 있는 값과 디퓨즈 맵을 혼합

COLORARG1[1] = CURRENT;

COLORARG2[1] = TEXTURE;

COLOROP  [1] = MODULATE2X;

COLOROP  [2] = DISABLE;

 

ht22_bump_fixed.zip Tfactor와 법선 맵의 내적 연산과 디퓨즈 맵과 MODULATE, MODULATE2X, MODULATE4X 혼합을 보여주고 있습니다.

 

<Fixed Function Pipeline Normal Mapping. ht22_bump_fixed.zip>

 

범프 효과를 고정 기능 파이프라인에서 간단하게 구현할 수 있지만 문제는 정점의 법선 벡터와 관련 없이 오직 빛의 방향만으로 설정된다는 것입니다. 과거 쉐이더가 지원 되지 않을 때 이 문제를 해결하기 위해서 다음 공식을 이용했었습니다.

 

반사의 밝기 = dot(정점의 법선 벡터, 빛의 방향 벡터)

               * 반사에 대한 법선 벡터 연산 결과

 

이것을 구현하려면 제일 처음 제시했던 방법을 수정해서 Color Op를 텍스처만 선택되지 않고 Diffuse Modulate할 수 있게 다음과 같이 수정해야 합니다.

 

COLORARG1[0] = TEXTURE;

COLORARG2[0] = DIFFUSE;

COLOROP  [0] = MODULATE;

RESULTARG[0] = CURRENT;

 

이것은 조명이 활성화 되어 있다면 정점 처리 과정의 Diffuse 색상은 조명과 정점의 법선 벡터 연산의 결과를 저장하고 있기 때문입니다.

 

다음으로 쉐이더를 사용한 범프 효과를 구현해 보겠습니다. 쉐이더를 사용하면 정점의 법선 벡터를 반영해서 좀 더 정교한 범프 효과를 구현 할 수 있습니다.

텍스처의 색상을 법선 벡터로 바꾸기 위해서 먼저 색상의 범위가 [0, 255]가 아닌 [0,1] 범위라는 것을 명심해야 합니다. 따라서 이것을 조명에 필요한 법선 벡터의 범위 [-1, 1]로 만들기 위해서 텍스처에서 얻은 색상에 2배를 하고 (-1)을 더해야 합니다.

 

텍스처의 법선 벡터 = 2* tex2D(노말 맵 샘플러, 텍스처 좌표).xyz - 1.0

 

앞서 노말 맵은 고 폴리곤 작업 등을 통해서 제작이 된다고 했습니다. 그래픽 담당자들은 다음 그림처럼 x, y 평면에 z 방향이 위를 향하는 공간에 법선 벡터를 색상으로 저장합니다. 이렇게 Z축이 D3D Y축 방향과 일치하게 되는 이유는 노말 맵 텍스처 작업은 Max와 같은 그래픽 툴을 이용하며 대부분의 그래픽 툴은 x, y 평면에 z축이 위를 향하는 오른손 좌표계로 구성되어 있기 때문입니다.

 

< 법선 맵 공간과 법선 벡터>

 

그리고 이 법선 맵은 다음 그림처럼 정점에 매핑이 되면 노말 맵 텍스처 공간의 z 방향은 정점의 법선 벡터(Normal Vector), 텍스처 공간의 y 방향 벡터는 정점의 접선 벡터(Tangent Vector), 그리고 x 방향 벡터는 종법선 벡터(Binormal Vector)에 대응 시키고 색상에서 추출한 법선 벡터를 이 세 개의 축인 정점의 법선, 접선, 종법선 벡터 공간으로 변환해야 합니다.

 

<색상에서 추출한 법선 벡터와 정점의 법선, 접선, 종법선 벡터로 구성된 공간>

 

법선, 접선, 종법선 벡터로 구성된 공간으로 텍스처에서 얻은 법선 벡터를 변환해야 하는데 이것을 쉽게 풀기 위해서 텍스처 공간의 x(1, 0, 0), y (0, 1, 0), 그리고 z (0, 0, 1)은 어떤 변환 행렬 M에 의해 각각 종법선(Bx, By, Bz), 접선(Tx, Ty, Tz), 법선(Nx, Ny, Nz) 벡터로 만들어 진다고 합시다.

이것을 수식으로 표현 다음과 같습니다.

 

, ,

 

세 개의 축이 변환하는 것을 하나로 다음과 같이 합칠 수 있으며 이를 통해서 행렬 M을 아주 손쉽게 구할 수 있습니다.

 

 

 

그런데 정점의 종법선, 접선, 법선 벡터들은 월드 행렬의 회전 변환을 할 수 있습니다. 따라서 회전 변환이 적용된 M'도 이와 같은 방법으로 구성합니다.

 

, ,  

 

이 회전 변환 행렬 M'에 색상에서 얻은 법선 벡터(TxN.x, TxN.y, TxN.z)를 변환하면 되는데 이것은 행렬 * 벡터 연산과 같은 의미입니다. , 최종 법선 벡터는 "M' * TxN"으로 만들어 집니다.

 

 

이것을 HLSL로 작성해야 하는데 픽셀 처리에서는 행렬 연산이 불가능 할 수 있으므로 행렬 * 벡터 연산을 풀어서 최종 법선 벡터를 만드는 것이 좋습니다.

 

TxN'.x = B.x' * TxN.x + T.x' * TxN.y + N.x' * TxN.z

TxN'.y = B.y' * TxN.x + T.y' * TxN.y + N.y' * TxN.z

TxN'.z = B.z' * TxN.x + T.z' * TxN.y + N.z' * TxN.z

 

이렇게 구한 TxN'을 분산 광원 효과, Specular 효과의 공식에서 사용되는 법선 벡터로 정해서 사용하면 됩니다.

 

지금까지의 내용이 맞는지 확인 하기 위해서 다음과 같이 색상이 (127, 127, 255)로 구성된 텍스처를 준비합니다.

 

 

색상 값 (127, 127, 255)은 쉐이더에서 (0.5, 0.5, 1.0)이 되며 이 값에 2를 곱하고 1을 빼면 (0, 0, 1)이 됩니다. 이와 같은 텍스처를 구(Sphere) 형태의 물체에 매핑하고 조명을 적용했을 때 정점에 적용된 조명 효과와 같으면 범프 효과는 성공인 것이고 쉐이더 코드는 제대로 작성된 것이라 할 수 있습니다.

 

구체(Sphere)는 종법선, 접선, 법선 벡터를 갖고 있다면 다음과 같이 정점 구조체를 구성합니다.

 

struct VtxNUV1

{

        VEC3    p;      // Position

        VEC3    n;      // Normal

        FLOAT   u, v;   // Texture Coord

        VEC3    t;      // Tangent

        VEC3    b;      // Binormal

};

 

또한 정점 선언자는 이 구조체의 자료 순서에 맞게 작성합니다.

 

D3DVERTEXELEMENT9 decl[] =

{

        {0, 0,D3DDECLTYPE_FLOAT3,D3DDECLMETHOD_DEFAULT,D3DDECLUSAGE_POSITION,0},

        {0,12,D3DDECLTYPE_FLOAT3,D3DDECLMETHOD_DEFAULT,D3DDECLUSAGE_NORMAL  ,0},

        {0,24,D3DDECLTYPE_FLOAT2,D3DDECLMETHOD_DEFAULT,D3DDECLUSAGE_TEXCOORD,0},

        {0,32,D3DDECLTYPE_FLOAT3,D3DDECLMETHOD_DEFAULT,D3DDECLUSAGE_TANGENT ,0},

        {0,44,D3DDECLTYPE_FLOAT3,D3DDECLMETHOD_DEFAULT,D3DDECLUSAGE_BINORMAL,0},

        D3DDECL_END()

};

 

이렇게 작성하는 것이 정석이지만 고정 기능 파이프라인의 FVF로 작성할 수 있습니다. 보통 텍스처 좌표는 하나의 정점에 하나 또는 2개 정도만 사용하는 것이 대부분이어서 사용할 수 있는 총 8개의 텍스처 좌표 중에서 남는 것을 3차원으로 하고 접선(Tangent), 종법선(Binormal)을 저장합니다.

 

enum

{

        FVF = D3DFVF_XYZ|D3DFVF_NORMAL|D3DFVF_TEX3|  \

               D3DFVF_TEXCOORDSIZE3(1)|D3DFVF_TEXCOORDSIZE3(2)

};

 

D3DFVF_TEX3 3개의 텍스처 좌표를 장을 사용하는 것을 의미하고 D3DFVF_TEXCOORDSIZE3(1) 1 번째의 텍스처 좌표를 3차원으로 지정함을 의미합니다.

 

아직까지 쉐이더가 익숙하지 않은 분들은 FVF를 사용하는 것이 유리해 보입니다. 다음으로 HLSL를 작성합니다. 먼저 정점 처리의 출력 구조체에 종법선, 접선, 법선 벡터를 텍스처 좌표계에 저장될 수 있는 구조를 만듭니다.

 

// 종법선, 접선, 법선 벡터를 저장하기 위한 정점 출력 구조체

struct SVsOut

{

        float4 Pos : POSITION;

        float2 Tex : TEXCOORD0;       // 디퓨즈 맵 텍스처 좌표

        float3 Bnr : TEXCOORD5; // 종법선 벡터

        float3 Tan : TEXCOORD6;       // 접선 벡터

        float3 Nor : TEXCOORD7;       // 법선 벡터

};

 

접선, 종법선 벡터를 텍스처 좌표를 사용했을 경우 입력 Semantic TEXCOORD#으로 지정합니다. DECLUSAGE를 사용했을 경우 다음의 접선 벡터에 대한 Semantic TANGENT, 종법선 벡터에 대한 Semantic BINORMAL를 사용합니다

정점 처리 함수에서 접선, 종법선 벡터는 법선 벡터와 마찬가지로 회전 변환만 적용하고 이것을 정점 출력 구조체 변수에 저장합니다.

 

// 정점 처리 함수

SVsOut VtxPrc(float3 Pos : POSITION   // 입력 정점 위치 벡터

            , float4 Nor : NORMAL     // 법선 벡터

            , float2 Tex : TEXCOORD0  // 텍스처 좌표

            , float3 Tan : TEXCOORD1  // 접선 벡터. 텍스처 좌표 Semantic 사용

            , float3 Bnr : TEXCOORD2  // 종법선 벡터. 텍스처 좌표 Semantic 사용

        float3 N = mul(Nor, m_mtRot); // 법선 벡터의 회전

        float3 T = mul(Tan, m_mtRot); // 접선 벡터의 회전

        float3 B = mul(Bnr, m_mtRot); // 종법선 벡터의 회전

        Out.Nor = N;                   // 회전 변환한 법선 벡터 저장

        Out.Tan = T;                  // 회전 변환한 접선 벡터 저장

        Out.Bnr = B;                  // 회전 변환한 종법선 벡터 저장

        return Out;

 

정점 처리 함수는 법선, 접선, 종법선 벡터의 회전이 있을 뿐 다른 정점 처리 과정과 크게 다른 점은 없습니다.

픽셀 처리는 최소한 픽셀 쉐이더 2.0 이상 지원되는 GPU가 필요합니다. 이것은 픽셀 쉐이더 조명 때와 마찬가지로 정점의 접선, 종법선 벡터는 픽셀 처리기에 의해 선형 보간이 되고 이 보간된 벡터를 다시 단위 벡터로 만들어야 하는데 이 정도의 명령어 처리는 2.0이상 되어야 제대로 구현 되기 때문입니다.

픽셀 처리 함수는 먼저 입력 받은 법선, 접선, 종법선 벡터를 단위 벡터로 만듭니다.

 

// 픽셀 처리 함수

float4 PxlPrc(SVsOut In) : COLOR0

        float3 B= normalize(In.Bnr);  // 입력 종법선 벡터를 단위 벡터로 만듦

        float3 T= normalize(In.Tan);  // 입력 접선 벡터를 단위 벡터로 만듦

        float3 N= normalize(In.Nor);  // 입력 법선 벡터를 단위 벡터로 만듦

 

다음으로 노말 맵에서 색상을 추출해서 이 값을 [-1, 1] 범위의 벡터로 만듭니다. 이를 위해서 tex2D()함수로 추출한 색상 값에 2를 곱하고 1을 뺍니다.

 

        float3 C1= 0;

        float3 C = 2*tex2D(SampNor, In.Tex ).xyz-1;

        C = normalize(C);

 

색상에서 추출한 이 법선 벡터를 행렬의 변환과 같은 연산을 합니다.

 

        C1.x = B.x * C.x + T.x * C.y + N.x * C.z;

        C1.y = B.y * C.x + T.y * C.y + N.y * C.z;

        C1.z = B.z * C.x + T.z * C.y + N.z * C.z;

 

마지막으로 dot() 함수 등을 사용해서 분산 조명 효과를 적용 합니다.

 

        Bmp = dot(C1, Lgt);           // 분산 조명 적용

        return Bmp;

 

 

<(0,0,1) 방향이 저장된 노말 맵을 사용한 분산 조명 효과

: ht22_bump1.zip, ht22_bump2.zip, ht22_bump3.zip>

 

(0, 0, 1) 방향으로 저장된 텍스처를 사용해서 원하는 형태의 조명 효과가 나왔으면 다음과 같은 노말 맵을 적용해 봅니다.

 

 

<노말 맵과 분산 조명 효과: ht22_bump1.zip, ht22_bump2.zip, ht22_bump3.zip>

 

종법선 벡터는 정점의 법선 벡터와 접선 벡터의 외적으로 구할 수 있습니다.

 

종법선 벡터 = cross(법선 벡터, 접선 벡터)

 

이 원리를 이용하면 정점 구조체와 데이터의 크기는 작아집니다. 그런데 픽셀 처리 함수는 종법선 벡터를 사용해야 하기 때문에 입력한 법선 벡터, 접선 벡터, 그리고 cross() 함수를 사용해서 종법선 벡터를 구합니다.

 

        float3 T= normalize(In.Tan);

        float3 N= normalize(In.Nor);

        float3 B= normalize(cross(T, N));     // 외적으로 종법선 구함

 

이 방법을 사용하면 픽셀 처리에서 cross() 함수를 사용하지만 정점의 데이터는 줄일 수 있으며 ht22_bump2.zip은 이것을 구현한 예제입니다.

 

모든 렌더링 물체가 접선 벡터 또는 종법선 벡터가 있으면 완전한 범프 효과를 만들 수 있지만 없는 경우에도 법선 벡터 만으로도 접선, 종법선 벡터를 외적을 이용해서 만들 수 있습니다. 예를 들어 데카르트 좌표계의 x, y, z축 방향 벡터는 서로 직각이며 이런 경우 x = cross(y, z), y = cross(z, x), z = cross(x, y)의 성질이 있습니다.

이것을 법선, 종법선, 접선에 적용하면 종 법선 벡터를 x축에 평행한 (1, 0, 0) 방향으로 정하고 접선 벡터를 법선 벡터와 (1, 0, 0)방향의 종법선 벡터의 외적으로 구하고 단위 벡터로 만듭니다. 다음으로 접선 벡터와 법선 벡터를 외적하고 이 벡터를 단위 벡터로 만들어 종법선 벡터로 설정합니다.

 

종법선 벡터 = float3(1, 0, 0)

접선 벡터 = normalize( cross(법선 벡터, 종법선 벡터) )

종법선 벡터 = normalize( cross(접선 벡터, 법선 벡터) )

 

HLSL로 작성하면 다음과 같습니다.

 

        float3 B= {1,0,0};     // 종법선 벡터

        float3 T= {0,1,0};     // 접선 벡터

        float3 N= normalize(In.Nor);

        T = normalize(cross(N, B));

        B = normalize(cross(T, N));

 

<ht22_bump3.zip>

 

이 방법은 3가지 문제가 발생할 수 있는데 먼저 법선 벡터가 (1,0,0)과 평행이면 평행 벡터의 외적은 0 벡터가 되는 특성에 의해서 접선 벡터가 0 벡터가 됩니다. 0 벡터가 아니라도 두 번째 문제인 접선 벡터의 방향입니다. 무조건 (1, 0, 0) 방향으로 종법선을 설정했기 때문에 반대 방향이 되면 반사의 밝기는 (+)에서 (-)가 되거나 (-) (+)가 됩니다. 세 번째 문제는 거의 정사각형 형태에서 노말 맵에서 샘플링 되어야 하는데 밀린 평행사변 형 형태가 되어 텍스처에서 정밀한 법선을 추출하기 어려울 수도 있습니다.

이들 3가지 문제 중에서 2번째가 가장 크게 눈에 띄고 그 다음 첫 번째이며 3번째의 경우는 잘 보이지 않습니다. 이렇게 정점의 법선 만으로 범프 효과를 만드는 것이 문제 점이 있지만 이 효과를 적용 안 하는 것보다 구현하는 것이 훨씬 좋습니다.

 

ht22_bump4.zip는 정점의 법선만으로 픽셀 처리과정에서 접선, 종법선을 구해서 범프 효과를 구현한 예입니다. 밝기와 Contrast를 위해서 내적의 결과에 상수를 더했으며 pow() 함수를 이용해서 밝은 부분은 더 밝게, 어두운 부분은 더 어둡게 표현되도록 구현했습니다.

 

        Bmp = dot(C1, Lgt);    // 분산 조명 효과

        Bmp += 0.85;           // 전체 밝기를 올림

        Bmp = pow(Bmp, nPower); // pow() 함수를 사용해서 Contrast를 높임

 

<디퓨즈, 법선 맵, 범프 효과: ht22_bump4.zip>

 

<디퓨즈 맵 + 범프 효과 - 1.2, 2.2, 4.2 : ht22_bump4.zip>

 

범프 효과를 사용하면 평범한 디퓨즈 매핑에 입체감 있는 렌더링을 표현할 수 있으며 이것은 이후 Specular 매핑과 결합되어 좀 더 사실감 있는 효과를 만들어 냅니다.

 

 

3.3 Specular Mapping

스페큘러 맵(Specular Map)은 일종의 Highlight Map으로 스페큘러에 적용되는 반사의 세기와 Sharpness 값이 저장된 텍스처입니다. 퐁 쉐이딩 등의 스페큘러 효과를 구현하는 스페큘러 매핑(Specular Mapping)은 이 텍스처의 값을 가지고 조명의 반사 효과의 변수로 사용합니다.

 

스페큘러 맵을 사용해서 다음과 같은 간단한 공식으로 최종 색상을 결정할 수도 있습니다.

 

최종 색상 = 조명 효과 + Specular Map

 

 

<스페큘러 맵과 반사 효과: ht23_spc_ati.zip>

 

조명 효과에 Highlight 색상을 더하는 방법은 구현하기가 쉬어 고정 기능 파이프라인에서부터 자주 사용되던 방법입니다. 처리 방법이 간단해서 대부분의 코드는 조명 효과에 집중되어 있으며 만약 범프 효과와 같이 구현 된다면 최종 색상은 이 둘의 효과를 더해서 만들어 집니다.

 

최종 색상 = 범프 효과 + Specular Mapping 효과

 

이 공식에 대해서 고정 기능 파이프라인의 다중 텍스처 처리 상태 값은 범프 효과를 먼저 처리하고 그 다음 단계에서 스페큘러 맵의 색상을 더하는 색상 혼합(Color Op) ADD로 처리합니다.

 

        TEXTUREFACTOR= (m_dTFactor);

        // Tfactor와 노말 맵의 dot 연산

        COLORARG1[0] = TFACTOR;

        COLORARG2[0] = TEXTURE;

        COLOROP  [0] = DOTPRODUCT3;

        RESULTARG[0] = CURRENT;

 

        // 디퓨즈 맵과 Modulate 2x 연산

        COLORARG1[1] = CURRENT;

        COLORARG2[1] = TEXTURE;

        COLOROP  [1] = MODULATE2X;

 

        // 스페큘러 맵과 Add 연산

        COLORARG1[2] = CURRENT;

        COLORARG2[2] = TEXTURE;

        COLOROP  [2] = ADD;

 

<ht23_spc_fixed.zip>

 

고정 기능 파이프라인에서도 간단하게 구현이 되지만 Highlight에 대한 색상을 정하려면 여러 단계의 멀티 텍스처 처리를 하거나 아니면 간단하게 색상이 있는 스페큘러 맵을 만들어야 합니다. 그런데 쉐이더를 사용하면 단색의 스페큘러 텍스처에 색상을 넣거나 밝기와 Contrast를 높일 수도 있습니다.

 

float4 PxlPrc2(SVsOut In) : COLOR

{

        float4 Hgt = tex2D( SampSpc, In.Tex );       // 스페큘러 맵에서 색상 추출

        float4 Hue={1.0, 0.6, 0.3, 1.0};

 

        Hgt *= Hue;                   // 색상 Shift

        Hgt *= 2.5;                   // 전체 밝기를 높임

        Hgt = pow(Hgt, 2.5)*1.5;      // pow() 함수로 Contrast 올림

 

        return Hgt;

}

 

 

<쉐이더를 사용해서 색 변환이 가해진 스페큘러 텍스처>

 

이렇게 단색의 색상에 특정 색상을 곱하고 pow() 함수 등을 사용해서 만든 스페큘러 매핑의 효과를 이전의 범프 효과와 결합 하면 좀 더 멋진 효과를 만들어 낼 수 있습니다. 주의 해야 할 것은 곱셈과 pow() 함수의 사용이 많아지면 전체 밝기가 어두워질 수 있습니다. 따라서 픽셀 처리 중간 마다 전체 밝기를 올리는 것이 중요합니다.

스페큘러 매핑과 범프 매핑을 혼합하기 위해서 범프 효과를 만드는 함수를 따로 작성하는 것이 좋습니다.

 

// 범프 효과 처리 함수

float4 NorPrc(float3 In_Nor, float2 In_Tx)

        N = normalize(In_Nor);

        T = normalize(cross(N, B));

        B = normalize(cross(T, N));

        float3 C = 2*tex2D(SampNor, In_Tx ).xyz-1;

        // 색상에서 추출한 법선 벡터의 회전

        C1.x = B.x * C.x + T.x * C.y + N.x * C.z;

        C1.y = B.y * C.x + T.y * C.y + N.y * C.z;

        C1.z = B.z * C.x + T.z * C.y + N.z * C.z;

        Bmp = dot(C1, Lgt) + 0.40;

        return Bmp;

 

범프 효과를 처리하는 함수와 스페큘러 텍스처의 색상을 처리하는 함수를 만들면 범프 효과 + 스페큘러 매핑 효과를 만들 수 있습니다.

 

float4 PxlPrc3(SVsOut In, uniform float4 Hue) : COLOR

        float4 TxD = tex2D( SampDif, In.Tex );       // 디퓨즈 텍스처 색상

        float4 Bmp = NorPrc(In.Nor, In.Tex);  // 범프 밝기

        float4 Hgt = tex2D( SampSpc, In.Tex );       // 스페큘러 맵 색상

 

        Hgt *= Hue;                   // 색상 설정

        Hgt *= 2.5f;                   // 밝기 올림

        Hgt = pow(Hgt, 2.5)*1.5;      // pow() 함수로 Contrast 올림

 

        Out = 4*TxD * Bmp;            // 전체 밝기 올림

        Out = pow(Out, 2.5);

        Out += Hgt;                   // 범프 * 디퓨즈 텍스처 + 스페큘러

        Out *=Hue;                    // 색상을 맞춤

 

        return Out;

 

<범프 효과, 스페큘러 효과>

<색상을 가한 범프 효과 + 스페큘러 효과: ht23_spc_shader.zip>

 

실제 렌더링 물체는 평면이 아닌 3차원 입체로 구성되어 있고 정점의 법선 벡터와 빛의 방향 벡터를 이용한 조명의 Specular 효과를 스페큘러 매핑에서 적용해야 합니다. 간단한 방법은 이 둘을 곱해서 Highlight를 만드는 것입니다.

 

float4 PxlPrc(SVsOut In) : COLOR

        float4 Hgt = tex2D( SampSpc, In.Tex );       // 스페큘러 맵에서 색상 추출

        // 조명의 스페큘러 효과 계산

        float3 R = normalize(In.Rfc);

        float3 E = normalize(In.Eye);

        float4 S = saturate(dot(R, E));              // 퐁 반사

        S = pow(S, m_fShrp);

 

        // 스페큘러 맵 색상과 조명의 스페큘러 반사 세기를 곱함

        Hgt *= S;

        Out += Hgt;                           // 최종 색상에 더함

        return Out;

 

  

<Specular 조명 효과를 반영한 범프 + 스페큘러 매핑: ht23_spc+bump.zip>

ht23_spc+bump.zip 예제는 조명의 반사 세기를 먼저 계산했지만 스페큘러 맵과 퐁 반사 과정의 dot() 처리 결과를 먼저 곱하고 pow() 함수를 적용할 수도 있습니다. 이 방법이 논리적으로 맞아 보이는데 문제는 스페큘러 맵의 반사가 아주 작은 일부의 영역이 되면 Highlight는 거의 보이지 않을 수도 있습니다.

 

 

3.4 Environment Mapping

환경 매핑(Environment Mapping)은 저 수준 쉐이더와 서피스 강의에서 이미 구현을 해보았습니다. 간단하게 정리한다면 환경 매핑은 3D 물체에 주변의 경관에 대한 반사 또는 굴절을 표현하는 것으로 이를 구현하는 과정은 3D 장면을 텍스처에 저장하고, 다음으로 고정 기능 파이프라인에서 환경 매핑을 구현할 수 있는 상태 값을 설정하거나 아니면 쉐이더로 매핑 텍스처 좌표를 설정합니다.

실시간 장면을 저장하기 위해서 Sphere Map 또는 Cube Map을 사용합니다. Sphere Map은 한 장의 텍스처에 장면을 저장한 것이고 Cube Map은 카메라의 앞쪽, 뒤쪽, 왼쪽, 오른쪽, , 아래 6방향에 대해서 장면을 저장한 텍스처 입니다.

 

 

<Sphere Map Cube Map>

 

단순히 반사에 대해서만 환경 매핑을 구현한다면 Sphere Map이 유리할 수도 있지만 환경 매핑은 반사(Reflection)뿐만 아니라 굴절(Refraction) 효과를 표현하기도 해서 Cube Map을 사용하는 것이 바람직합니다.

아직까지 환경 매핑이 익숙하지 않기 때문에 먼저 저 수준 쉐이더 강의에서 구현한 환경 매핑을 HLSL로 다시 구현해 보겠습니다. 다음으로 Cube Map을 만들어서 반사와 굴절을 표현해 보고 마지막으로 반사, 굴절, 그리고 Diffuse Map을 동시에 적용해서 반 투명 반사 물체를 구현해 보겠습니다.

Sphere Map을 가지고 반사 효과를 만드는 방법은 다음 그림의 붉은 색 화살표에 대한 텍스처 좌표를 정점의 법선 벡터로 사용하면 간단하게 반사 효과를 만들 수 있음을 저 수준 쉐이더 시간에 살펴보았습니다.

 

텍스처 좌표'  = 회전 변환된 정점의 법선 벡터 * 뷰 행렬

텍스처 좌표.x = 텍스처 좌표'.x * 0.5 + 0.5

텍스처 좌표.y = -텍스처 좌표'.y * 0.5 + 0.5

 

3D 렌더링 물체는 회전할 수도 있기 때문에 법선 벡터는 먼저 회전 변환을 적용하고 텍스처 좌표로 사용하기 위해서 뷰 변환을 합니다.

 

// 정점 처리 함수

SVsOut VtxPrc(float4 Pos : POSITION0, float3 Nor : NORMAL0)

{

        SVsOut  Out = (SVsOut)0; // 출력 데이터 초기화

        float3  N = Nor;

        float2  T = 1.0;

 

        N = mul(N, m_mtRot);   // 법선 벡터의 회전 변환

        N = mul(N, m_mtViw);   // 법선 벡터의 뷰 변환

 

        T   = N.xy;            // 텍스처 좌표 설정

        T.y = -T.y;

        T   = T * 0.5 + 0.5;

 

<Sphere Map 반사 효과: ht25_env1_sphere1.zip>

 

장면을 텍스처에 저장하는 방법은 후면 버퍼의 색상 버퍼(또는 서피스)를 텍스처의 서피스로 대처하는 방법과 ID3DXRenderToEnvMap 객체를 사용하는 방법 두 가지가 있습니다. 이 둘의 사용은 고정 기능 파이프라인의 서피스 강의에 자세히 나와 있으므로 생소한 분들은 그 것을 참고 하기 바라며 여기서는 ID3DXRenderToEnvMap를 사용해서 Sphere, Cube Map에 적용하도록 하겠습니다.

 

ID3DXRenderToEnvMap는 사용하고 있는 후면 버퍼의 색상, 깊이, 스텐실에 대한 Format을 가지고 D3DXCreateRenderToEnvMap() 함수를 사용해서 객체를 생성합니다. 후면 버퍼의 색상 버퍼는 디바이스의 GetRenderTarget() 함수를 사용해도 되지만 이 함수는 D3DCREATE_PUREDEVICE로 만들었을 경우에 제대로 동작하지 않을 수 있으므로 GetBackBuffer() 함수를 사용합니다. 깊이-스텐실 버퍼는 GetDepthStencilSurface() 함수를 사용합니다.

 

const   int     ENVMAP_RESOLUTION      = 256;

typedef LPD3DXRenderToEnvMap  PDRE;

PDRE    m_pRndEnv;

 

HRESULT CMain::Restore()

        LPDIRECT3DSURFACE9     pSrf;   // 색상, 깊이와 스텐실 버퍼용 서피스

        D3DSURFACE_DESC               dscC;   // 색상 버퍼 정보

        D3DSURFACE_DESC               dscD;   // 깊이 버퍼 정보

        // 색상 버퍼 가져오기

        if(FAILED(pDev->GetBackBuffer(0, 0, D3DBACKBUFFER_TYPE_MONO, &pSrf)))

               return-1;

 

        // 색상 버퍼의 정보 가져오기

        pSrf->GetDesc( &dscC);

        pSrf->Release();

 

        // 깊이, 스텐실 버퍼 가져오기

        if(FAILED(pDev->GetDepthStencilSurface(&pSrf)))

               return -1;

 

        // 깊이, 스텐실 버퍼의 정보 가져오기

        pSrf->GetDesc(&dscD);

        pSrf->Release();

 

        // 렌더링 환경 매핑 객체 생성

        hr = D3DXCreateRenderToEnvMap( pDev, ENVMAP_RESOLUTION, 1

                       , dscC.Format, TRUE, dscD.Format, &m_pRndEnv );

 

장면을 저장하기 위한 Sphere Map D3DXCreateTexture() 함수로 생성을 하며 좀 더 빠르게 처리할 수 있도록 이 함수에 Memory Pool D3DPOOL_DEFAULT로 하고 UsageD3DUSAGE_RENDERTARGET 으로 설정합니다. D3DUSAGE_RENDERTARGET 옵션이 실패하면 Usage 0으로 설정하고 다시 생성합니다.

 

typedef LPDIRECT3DTEXTURE9    PDTX;

PDTX    m_pTexSph;

// Sphere Map 생성

hr = D3DXCreateTexture( pDev

               , ENVMAP_RESOLUTION, ENVMAP_RESOLUTION

               , 1, D3DUSAGE_RENDERTARGET

               , dscC.Format, D3DPOOL_DEFAULT, &m_pTexSph);

 

if( FAILED( hr ) )

        hr = D3DXCreateTexture(pDev

               , ENVMAP_RESOLUTION, ENVMAP_RESOLUTION

               , 1, 0

               , dscC.Format, D3DPOOL_DEFAULT, &m_pTexSph );

 

전체 코드는 ht25_env1_sphere2.zip CMain 클래스를 참고 하기 바랍니다.

 

이렇게 렌더링 환경 매핑 객체, 그리고 장면을 저장할 텍스처를 만들었으면 3D를 텍스처에 저장하고 환경 매핑 처리에 연결하는 단계만 남아 있습니다.

3D 장면을 텍스처에 저장하기 위해서 카메라의 앞과 뒤에 해당하는 +z, -z, , 아래에 해당하는+y, -y, 그리고 왼쪽, 오른쪽에 해당하는 -x, +x 에 대한 총 6 방향에 대해서 뷰 행렬을 만들고 렌더링을 해야 합니다. D3D SDK HDRCubeMap Sample에는 이들 6방향에 대한 뷰 행렬을 만드는 예가 D3DUtil_GetCubeMapViewMatrix() 또는 DXUTGetCubeMapViewMatrix() 함수로 구현되어 있으며 이들 함수 행렬을 반환하는데 메모리 복사를 조금이라도 피하기 위해 다음과 같이 수정 했습니다.

 

void T_SetupCubeViewMatrix(D3DXMATRIX* pmtViw, DWORD dwFace )

{

    D3DXVECTOR3 vcEye   = D3DXVECTOR3( 0.0f, 0.0f, 0.0f );

    D3DXVECTOR3 vcLook;

    D3DXVECTOR3 vcUp;

 

    switch( dwFace )

    {

        case D3DCUBEMAP_FACE_POSITIVE_X:

            vcLook = D3DXVECTOR3( 1.0f, 0.0f, 0.0f );

            vcUp   = D3DXVECTOR3( 0.0f, 1.0f, 0.0f );

            break;

        case D3DCUBEMAP_FACE_NEGATIVE_X:

            vcLook = D3DXVECTOR3(-1.0f, 0.0f, 0.0f );

            vcUp   = D3DXVECTOR3( 0.0f, 1.0f, 0.0f );

            break;

        case D3DCUBEMAP_FACE_POSITIVE_Y:

            vcLook = D3DXVECTOR3( 0.0f, 1.0f, 0.0f );

            vcUp   = D3DXVECTOR3( 0.0f, 0.0f,-1.0f );

            break;

        case D3DCUBEMAP_FACE_NEGATIVE_Y:

            vcLook = D3DXVECTOR3( 0.0F,-1.0F, 0.0F );

            vcUp   = D3DXVECTOR3( 0.0F, 0.0F, 1.0F );

            break;

        case D3DCUBEMAP_FACE_POSITIVE_Z:

            vcLook = D3DXVECTOR3( 0.0F, 0.0F, 1.0F );

            vcUp   = D3DXVECTOR3( 0.0F, 1.0F, 0.0F );

            break;

        case D3DCUBEMAP_FACE_NEGATIVE_Z:

            vcLook = D3DXVECTOR3( 0.0F, 0.0F,-1.0F );

            vcUp   = D3DXVECTOR3( 0.0F, 1.0F, 0.0F );

            break;

    }

 

    // 카메라의 +x, -x, +y, -y, +z, -z 방향에 대한 행렬

    D3DXMatrixLookAtLH(pmtViw, &vcEye, &vcLook, &vcUp );

}

 

이렇게 카메라의 x, y, z 축 방향에 대한 6개의 행렬을 만들고 이를 카메라의 뷰 행렬과 곱하면 각 방향에 대한 뷰 행렬을 만들 수 있습니다.

 

D3DXMATRIX mtViwCur;   // 현재 장면의 뷰 행렬

pDev->GetTransform(D3DTS_VIEW, &mtViwCur);   // 디바이스에서 뷰 행렬 얻기

 

D3DXMATRIX mtViw[6];   // 카메라의 6 방향에 대한 행렬

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

{

        T_SetupCubeViewMatrix(&mtViw[i], (D3DCUBEMAP_FACES) i );

        mtViw[i] = mtViwCur * mtViw[i];

}

 

카메라의 각각의 축에 대한 뷰 행렬을 만들었으면 다음으로 6번을 순회하면서 Sphere Map에 장면을 ID3DXRenderToEnvMap 객체의 도움을 받아 렌더링 합니다.

 

// Rendering to Sphere surface

m_pRndEnv->BeginSphere(m_pTxSph);

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

        {

               m_pRndEnv->Face( (D3DCUBEMAP_FACES) i, 0 );

               RenderScene( &mtViw[i], &mtPrj);

        }

m_pRndEnv->End(0);

 

실시간 장면을 저장한 텍스처를 환경 매핑을 구현한 CShaderEx 객체의 환경 매핑 텍스처로 설정하면 주전자의 표면이 주변을 반사한 효과를 만들 수 있습니다.

 

<장면을 저장한 Sphere Map으로 구현한 환경 매핑: ht25_env1_sphere3.zip>

 

반사 효과는 Sphere Map을 사용해도 충분합니다. 그런데 실 세계의 플라스틱 또는 유리병 같은 물체는 반사(Reflection)뿐만 하니라 굴절(Refraction) 효과도 만들어 냅니다. 이것을 3D 구현하려면 Sphere Map으로 더 이상 표현할 수 없습니다. 이렇게 굴절 효과까지 포함한 환경 매핑은 Cube Map을 사용해야 합니다.

Cube Map Sphere Map이 카메라 주변에 대해서 한 개의 텍스처를 사용하는 것에 반해서 각각의 6 방향에 대한 장면을 저장한 텍스처 입니다. 반사에 효과 자체는 Cube Map Sphere Map 둘 다 비슷하지만 구현하는 방법은 전혀 다릅니다. Cube Map Sphere Map과 다르게 그림처럼 카메라의 방향에 따라 장면 그 자체를 저장하고 있어서 적절할 UV를 만들고 텍스처에서 픽셀을 가져와야 합니다.

 

<Cube Map에 저장된 장면>

 

UV를 구성하고 픽셀을 가져오는 것이 어려워 보이지만 사용자 프로그래머는 Cube Map을 만들어 놓고 디바이스의 상태 값을 설정하거나 아니면 HLSL texCUBE() 함수로 간단히 픽셀을 가져올 수 있어서 이 부분은 걱정할 필요가 없습니다.

 

Sphere Map을 연습한 것처럼 먼저 미리 만들어진 Cube Map HLSL을 사용해서 렌더링 해보고 다음으로 실시간 장면을 Cube Map에 저장해서 렌더링 해봅시다. DX SDK의 예제에는 이미 장면을 저장해 놓은 "LobbyCube.dds" 라는 Cube Map 파일이 있습니다. 파일에 저장된 Cube Map의 텍스처는 IDirect3dCubeTexture9 객체가 필요하고 이 객체는 D3DXCreateCubeTextureFromFile() 함수를 사용해서 만듭니다.

 

typedef LPDIRECT3DCUBETEXTURE9 PDTC;

PDTC    m_pTxCbm;

D3DXCreateCubeTextureFromFile( m_pDev, "data/LobbyCube.dds", &m_pTxCbm );

 

Sphere Map이 법선 벡터를 직접 텍스처 좌표를 사용했는데 Cube Mapping 3차원 반사(Reflection) 벡터를 사용합니다. 이 반사 벡터를 HLSL texCUBE() 함수에 전달하면 디바이스는 Cube Map에서 픽셀을 가져옵니다.

 

// Cube 텍스처에서 픽셀을 처리하는 함수

float4  PxlPrc(float3 vcReflection) : COLOR0

{

        return texCUBE( Sampler_CubeMap, vcReflection);

}

 

정점 처리와 병행하는 것이 보통이므로 정점 처리에서 변환된 위치와 반사 벡터를 저장할 수 있도록 Cube Mapping에 대한 정점 처리 출력 구조체를 구성합니다.

 

struct SVsOut

{

        float4  Pos : POSITION0;       // 출력 위치

        float3  Rfc : TEXCOORD7;       // 반사 벡터

};

 

이제 정점 처리 함수를 작성해야 하는데 주의할 것은 출력에 대한 반사 벡터는 조명의 반사 중에서 퐁 반사 시간에 구현했던 반사 벡터와 거의 같은 방식으로 정점의 법선 벡터와 변환한 정점의 위치에서 카메라의 위치에 대한 방향인 시선 벡터를 사용해서 만듭니다.

그런데 조명과 다르게 이 반사 벡터는 뷰 공간(View Space or Camera Space)의 벡터 이여만 합니다. 이것은 실시간 장면을 저장한 텍스처는 카메라를 중심으로 만들어지기 때문입니다.

 

<반사와 굴절 벡터>

 

뷰 공간에서 반사 벡터를 만드는 방법은 간단하게 정점의 법선 벡터를 회전 변환 후에 다시 뷰 변환 과정을 추가하면 됩니다. 또한 뷰 변환 후 정점의 위치에서 카메라의 위치를 바라보는 시선 벡터는 정점 위치의 월드 변환 후 뷰 변환 후의 벡터를 정규화 하면 됩니다.

 

SVsOut VtxPrc(float3 Pos : POSITION0, float3 Nor : NORMAL0)

{

        SVsOut Out = (SVsOut)0;               // Initialize to Zero

        float4  P = float4(Pos,1);

        float3  N = Nor;

        float3  E = 1;

        float3  R = 1;

 

        N = mul(N, m_mtWld);          // 법선 벡터의 월드 변환

        N = mul(N, m_mtViw);          // 법선 벡터의 뷰 변환

        N = normalize(N);             // 단위 벡터로 만듦

 

        P = mul(P, m_mtWld);          // 정점 위치의 월드 변환

        P = mul(P, m_mtViw);          // 정점 위치의 뷰 변환

 

        E = -normalize(P);            // 시선 벡터

        R = 2.0 * dot(E, N) * N - E;  // 반사 벡터: reflect( -E, N );

        Out.Rfc = R;                  // 반사 벡터를 출력 레지스터에 복사

 

HLSL에서 벡터가 3차원 이면 float4x4 행렬과 곱셈을 해도 float3x3 행렬과의 곱셈과 같습니다. 또한 렌더링 물체에 같은 Scale이 적용이 되면 법선 벡터를 월드 변환과 뷰 변환을 진행한 후에 정규화 하면 뷰 공간에서의 법선 벡터가 됩니다.

 

<장면을 저장한 Cube Map으로 구현한 환경 매핑: ht25_env2_cube1.zip>

 

Cube Mapping에 대한 HLSL을 작성했고, 다음으로 장면을 Cube Map에 저장하는 단계인데 이 과정은 Sphere Map에서와 거의 같으며 먼저 D3DXCreateCubeTexture() 함수를 사용해서 Cube Map 객체를 생성합니다.

 

hr = D3DXCreateCubeTexture( pDev, ENVMAP_RESOLUTION

                       , 1, D3DUSAGE_RENDERTARGET

                       , dscC.Format, D3DPOOL_DEFAULT, &m_pTexCbm);

if( FAILED( hr ) )

        hr = D3DXCreateCubeTexture(pDev, ENVMAP_RESOLUTION

                       , 1, 0, dscC.Format, D3DPOOL_DEFAULT, &m_pTexCbm );

 

<ht25_env2_cube2.zip>

 

Sphere Map과 마찬가지로 UsageD3DUSAGE_RENDERTARGET 으로 해서 하드웨어 가속을 받게 합니다. 이 옵션이 실패하면 Usage 0으로 하고 다시 생성합니다. 전체 코드는 ht25_env2_cube2.zip을 참고 하기 바랍니다.

이제 남은 단계는 장면을 Cube Map에 저장하는 과정입니다. 이것은 Sphere Map에서와 같으며 차이는 ID3DXRenderToEnvMap 객체의 BeginSphere 대신 BeginCube() 함수를 호출을 시작으로 카메라의 6방향에 따라 장면을 Cube Map에 저장합니다.

 

m_pRndEnv->BeginCube(m_pTxCbm);

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

        {

                m_pRndEnv->Face( (D3DCUBEMAP_FACES) i, 0 );

               RenderScene( &mtViw[i], &mtPrj);

        }

m_pRndEnv->End(0);

 

<Cube Map을 사용한 반사 효과: ht25_env2_cube3.zip Key-"1">

 

Cube Map을 사용하면 굴절 효과도 구현할 수 있습니다. 간단한 파동의 굴절 법칙은 Snell의 법칙을 사용합니다. Snell의 법칙은 매질 n1 n2에 대해서 입사각 θ1와 굴절각 θ2에 대해서 다음과 같은 관계식을 표현한 것입니다.

 

Snell의 법칙: n1 * sinθ1 = n2 * sinθ2

 

이 법칙을 이용해서 우리는 입사된 빛의 방향 대신 시선 벡터, 법선 벡터, 그리고 매질 n1 n2를 가지고 굴절 벡터 를 구할 수 있습니다.

 

 

Snell의 법칙에서 얻고 으로 구합니다. 또한 방향에 대한 길이와 같고  방향의 벡터는 법선 벡터와 시선 벡터로 다음과 같이 구할 수 있고 이 벡터의 길이가 가 됩니다.

 

 방향의 벡터 =

= Length( 방향의 벡터) = Length()

( k = n1/ n2),

 

이것을 HLSL로 바꾸는 작업이 필요한데 픽셀 기반 조명에서처럼 정점 처리 과정은 법선과 시선 벡터만 구하고 굴절 벡터는 픽셀 처리 과정에서 구하기 위해서 정점 처리 결과를 저장할 구조체에 시선 벡터와 법선 벡터를 저장할 수 있도록 선언합니다.

 

struct SVsOut

{

        float4  Pos : POSITION0;

        float3  Eye : TEXCOORD6;       // 시선 벡터

        float3  Nor : TEXCOORD7;       // 법선 벡터

};

 

정점 처리 함수는 법선 벡터의 뷰 공간 변환, 시선 벡터의 뷰 공간 변환만 수행하고 이것을 출력 구조체에 저장합니다.

 

//정점 처리 함수

SVsOut VtxPrc(float3 Pos : POSITION0, float3 Nor : NORMAL0)

{

        SVsOut Out = (SVsOut)0;

        float4  P = float4(Pos,1);

        float3  N = Nor;

        float3  E = 1;

        N = mul(N, m_mtWld);   // 법선 벡터의 월드 변환

        N = mul(N, m_mtViw);   // 법선 벡터의 뷰 공간 변환

        N = normalize(N);      // 정규화

        P = mul(P, m_mtWld);   // 위치 벡터의 월드 변환

        P = mul(P, m_mtViw);   // 위치 벡터의 뷰 변환

        E = -normalize(P);     // 시선 벡터는 뷰 변환된 위치 벡터의 정규화와 같음

        Out.Eye = E;           // 시선 벡터 복사

        Out.Nor = N;           // 법선 벡터 복사

 

픽셀 처리과정은 Snell의 법칙으로 굴절 벡터를 구하고 이 벡터를 HLSL texCUBE() 함수의 인수로 전달하는 과정입니다.

먼저 static을 사용해서 두 매질에 대해서 정의 합니다.

 

static float   n1 = 1.00;     // n1 매질 굴절률

static float   n2 = 1.02;     // n2 매질 굴절률

 

// 픽셀 처리함수

float4  PxlPrc(SVsOut In) : COLOR0

        float3 E = normalize(In.Eye); // 입력된 시선 벡터의 정규화

        float3 N = normalize(In.Nor); // 입력된 법선 벡터의 정규화

        float3 F = 0;                 // 굴절 벡터

 

        float3 X = dot(E, N) * N - E; // 법선에 수직인 x 방향의 벡터

        float sin_theta1 = length(-X); // sinθ1 를 구함

        float k = n1/n2;              // 매질의 비율을 구함

        float sin_theta2 = k * sin_theta1;    // 스넬 법칙으로 sinθ2 를 구함

        // cosθ2 를 구함

        float cos_theta2 = sqrt( 1.0 - sin_theta2 * sin_theta2);

 

        X = normalize(X);             // X 방향의 벡터를 정규화

 

        // 정규화된 X 방향의 벡터, 법선 벡터, cosθ2, sinθ2 를 가지고

        // 굴절 벡터를 구함

        F = (-N) * cos_theta2 + X * sin_theta2;

 

        Out = texCUBE( SmpCbm, F);

 

Snell의 법칙을 사용해서 굴절 벡터를 직접 구현 했는데 HLSL은 반사에 대한 reflect() 함수가 있듯이 굴절에 대한 refract() 함수가 있습니다. reflect() 함수와 refract() 함수를 사용해서 이전의 쉐이더 코드를 다음과 같이 대처할 수 있습니다.

 

        float k = n1/n2;              // 굴절 비율

        R = reflect(-E, N);           // 반사 벡터

        F = refract(-E, N, k);        // 굴절 벡터

 

ht25_env2_cube3.zip Cube Map을 이용해서 반사와 굴절 효과를 표현한 예입니다. 숫자 키 1을 누르면 반사효과를, 숫자 키 2를 누르면 굴절 효과를 볼 수 있습니다.

 

<Cube Map을 사용한 굴절 효과: ht25_env2_cube3.zip Key-"2">

 

Cube Map은 반사와 굴절 모두를 표현할 수 있어서 플라스틱 병과 같은 거의 투명한 물체를 쉽게 표현할 수 있습니다. 이런 물체들은 빛의 입사각이 작으면 투과율이 높고 입사각이 크면 반사율이 높습니다. 입사각은 시선 벡터와 법선 벡터의 내적으로 구할 수 있으므로 투과율(또는 비중)을 이 둘의 벡터의 내적의 제곱으로 간단히 결정해 보도록 합시다.

 

투과율(w) :

        w = dot(반사 벡터 또는 시선, 법선 벡터);

        w = w*w;

 

또한 렌더링 물체에 약간의 Diffuse Map을 적용하면 사실감을 더 높일 수 있으며 ht25_env2_cube4.zip는 반사+굴절 효과에 Diffuse Map 20% 적용해서 구현한 예제입니다.

 

<Cube Map을 사용한 반사 + 굴절 + Diffuse Map 효과: ht25_env2_cube4.zip>

 

Cube Map을 사용한 환경 매핑은 법선 벡터를 사용하기 때문에 이 법선 벡터에 대해서 Normal Map을 적용하면 올록볼록한 표면의 반사와 굴절을 만들 수 있습니다. ht25_env2_cube5.zip Normal Map을 법선 벡터로 사용해서 환경 매핑을 구현한 예제입니다. 텍스처에서 법선 벡터를 구하는 함수는 이전의 범프 효과(Bump Effect)에서 사용한 함수를 거의 그대로 사용했습니다.

 

<Cube Map+Normal Map을 사용한 반사 + 굴절 효과: ht25_env2_cube5.zip>

 

범프 맵을 결합한 환경 매핑의 응용으로 물에 대한 반사 효과를 만들 수 있습니다. 물에 대한 효과는 파동 방정식(Wave Equation), 반사, 그리고 굴절에 대한 적절한 수식을 필요하기 때문에 향상된 기술을 선보이기 위해서 3D의 예제로 가장 많이 구현되고 있습니다.

 

보통 파동 방정식의 미분 방정식 형태는 다음과 같이 주어집니다.

 

 

이 미분 방정식을 풀기 위해서 조화 진동자(Harmonic Oscillator) 모델을 사용하고 있으며 점성(Damping)이 있는 조화 진동자의 풀이는 exp() 함수의 결합 형태로 풀이가 됩니다.

 

 

또한 푸아송 방정식에 의해서 하나의 파동은 여러 파동의 중첩(Super Position)으로 풀이가 가능하고 이 것을 적용하면 파동에 대한 최종 해는 각각의 파동을 더한 결과가 됩니다.

 

 

바다와 호수 등의 파동을 만드는 주요 요인은 바람입니다. 이 바람을 이용해서 수면의 운동을 구현하는 것이 가장 바람직할 수 있지만 여기서는 예제 수준 정도로만 만들어 보기 위해서 4개의 돌이 위 아래로 움직이고 이 돌에 의해 파동이 만들어지는 것을 가지고 물에 대한 효과를 만들어 보겠습니다.

물 분자의 운동은 격자가 일정한 정점의 위, 아래 움직임으로 표현할 수 있습니다. 그런데 정점 버퍼 또는 시스템 메모리에 만든 정점의 모든 위치를 직접 변경하는 것은 교체에 대한 부담이 큽니다. 따라서 정점 쉐이더에서 출력 위치의 수식을 만들어서 바꾸는 것이 좋습니다.

 

의 간단한 형태는  가 되고 이것을 HLSL로 쉽게 작성할 수 있습니다.

 

float3  WavePos(float3 Pos, float3 eps=float3(0,0,0))

        tPos.y += exp(-wvK.x * r)*sin(r * wvOmega.x - m_fTime * wvSpeed.x);

 

HLSLht25_env2_water1.zip 예제에 적용하면 ht25_env2_water2.zip와 같이 물결이 출렁이는 효과를 볼 수 있습니다.

 

 

<수면의 파동. ht25_env2_water1.zip, ht25_env2_water2.zip>

 

정점의 움직임을 완성했고 다음으로 수면의 반사와 굴절을 처리할 차례입니다. 이것은 이전의 범프 매핑을 거의 그대로 이용하는 것이 좋습니다. 동적인 효과를 만들기 위해서 정점의 UV 좌표가 시간에 의존하게 하는 것이 좋으며 또한 한 방향 보다 여러 방향으로 설정하는 것이 더 효과적입니다. 이를 정점 처리 함수에 적용합니다.

 

SVsOut VtxPrc(float3 iPos : POSITION0)

        float2  wvSpdU=float2(0.02f, +0.02f);

        float2  wvSpdV=float2(0.02f, -0.02f);

        float Time = m_fTime;

        float2  Tex= 1;

        float2  Ds1;    // Distortion UV1

        float2  Ds2;    // Distortion UV2

 

        Tex.x = iPos.x/16.;

        Tex.y = 1- iPos.z/16.;

        Ds1 = Tex.xy + wvSpdU * Time; // 시간에 의존하는 텍스처 좌표(UV1) 생성

        Ds2 = Tex.yx + wvSpdV * Time; // …

        Out.Ds1 = Ds1;

        Out.Ds2 = Ds2;

 

빛의 반사와 굴절은 프레넬(Fresnel) 방정식으로 풀이 되며 프레넬 방정식의 근사식은 다음과 구할 수 있습니다.

 

Fresnel 근사식 = F + (1-F)*cosθ^5

 

게임은 현실의 적당한 흉내내기 이므로 굴절률 n1, n2가 주어질 때 좀 더 간단한 형태의 프레넬 방정식을 만들 수 있습니다.

 

Simple Fresnel F = pow*(n2/n1-dot(-E,N))

 

이 근사식을 픽셀 처리 함수에 적용해서 반사 계수로 사용해서 굴절 효과까지 만들어야 하는데 여기서는 반투명 계수 정도로만 사용하도록 하겠습니다.

 

float4  PxlPrc(SVsOut In) : COLOR0

        float3 E = normalize(In.Eye);

        float  F;

        float   n1      = 1.0;

        float   n2      = 1.333;

 

        // Calculate Simple Fresnel = (F - I.N)^2

        F  = pow(F - dot(-E, N), 2);

        Out = texCUBE( SmpCbm, R);

        Out.a = F;

 

내용은 약간 길지만 어렵지 않으므로 전체 코드는 ht25_env2_water4.zip를 참고하길 바랍니다.

 

<수면 반사 효과: ht25_env2_water4.zip>

 

 

3.5 깊이 버퍼 그림자

그림자를 게임의 3D 장면에 구현하는 것은 게임을 만드는 과정 중에서 프로그래머에게 기쁨을 주는 작업 중의 하나 입니다. 현재 그림자를 만드는 방법은 간단한 원형 이미지를 캐릭터의 발 밑에 렌더링 하거나 3D 물체를 2차원 평면 텍스처에 저장하고 이 텍스처를 매핑 하는 투영 그림자 매핑 방법이 있습니다. 하드웨어 성능이 좋다면 좀더 향상된 방법으로 광원의 위치와 방향에 대해서 3D 물체의 깊이를 텍스처로 저장하고 이 텍스처의 깊이에 따라 그림자를 표현하는 깊이 버퍼 그림자가 있습니다. 또한 오래 전부터 사용되고 있으며 가장 멋진 그림자를 만드는 부피 그림자(Volume Shadow)가 있습니다. 이 방법은 이전 3D 기초 시간에서 프레임 버퍼의 스텐실 버퍼를 사용해서 구현해 보았습니다.

하드웨어, 3D 장면에 소모되는 그래픽 리소스, 장르 등에 따라서 간단한 그림자를 선택하거나 아니면 사실감 있는 그림자를 만들 수 있는데 그림자를 표현 하지 않는 것보다 어떤 식으로든 표현하는 쪽이 게임의 사실감을 더 높여 줍니다. 이것은 조명과 비슷해서 조명을 사용함으로써 부피 느낌을 만들 듯이 그림자는 객체와 주변의 환경에 대해서 공간 느낌을 형성하기 때문입니다.

여러 가지의 그림자를 구현 방법 중에서 원형 그림자는 구현 방법이 쉽기 때문에 그냥 넘어가겠습니다. 대신 쉐이더를 사용해야 쉽게 만들 수 있는 깊이 버퍼 그림자를 먼저 구현해 보도록 하겠습니다.

 

깊이 버퍼 그림자의 원리는 의외로 간단합니다. 그림처럼 만약 현재의 정점과 조명 사이에 어떤 정점이 존재하면 디퓨즈 색상을 어둡게 처리하는 것입니다. 예를 들어 그림의 A, B, C 픽셀을 조명에서 바라본 깊이 값을 d1, d2, d3로 계산이 되었다고 합시다. 다음으로 조명에서 바라본 최종 깊이 값을 결정할 때 픽셀 B는 픽셀 A로 가려지기 때문에 최종 깊이 값을 d2 가 아닌 d1을 가지도록 합니다.

 

<깊이 버퍼 그림자의 원리>

 

깊이 버퍼 그림자의 원리는 이렇게 조명에서 바라본 최종 깊이 값을 먼저 구성하고 다시 원래의 깊이 값과 비교를 하는 것입니다. A C 픽셀은 깊이 값의 변화가 없지만 픽셀 B는 자신의 깊이 값 d2보다 최종 깊이 값 d1이 작으므로 그림자 적용 대상이 되는 픽셀이 되는 것입니다.

 

간단한 내용이지만 어떻게 조명에서 바라본 최종 깊이 값들을 만들 수 있을 까요? 우리는 이전 쉐이더 기초 시간에 장면의 깊이 값을 텍스처에 저장하는 방법을 알고 있습니다. , 디바이스의 파이프 라인을 이용해서 조명의 위치와 방향에 의존하는 뷰 행렬과 투영 행렬을 가지고 뷰 변환, 정규 변환에 적용해서 장면에 사용된 물체들의 위치를 텍스처에 저장하는 것입니다.

그 다음으로 조명의 위치로부터 깊이를 다시 계산하고 이 값을 깊이가 저장된 텍스처의 값과 비교해서 값이 텍스처의 값보다 크면 그림자가 적용되는 픽셀로 판정을 합니다. 이 판정을 위해서 정점 쉐이더에서는 조명의 뷰 행렬, 투영 행렬로 렌더링 물체의 깊이를 계산하고 이 값을 픽셀 쉐이더로 넘깁니다. 픽셀 쉐이더 함수는 깊이가 저장된 텍스처에 색상을 추출해서 정점 처리에서 넘어온 값과 비교하는 코드를 작성합니다.

 

지금까지 깊이 버퍼 그림자를 구현 하는 내용을 간단히 살펴보았습니다. 이 내용을 구체적으로 구현하도록 하겠습니다. 첫 번째 해야 할 일은 조명의 위치와 방향으로 미리 저장된 텍스처가 존재한다는 가정 하에 이것을 그림자가 적용될 물체에 매핑 하도록 하는 것입니다. 이 방법은 3D 기초 시간에 연습했던 투영 매핑과 동일 하며 우리는 모든 처리를 쉐이더를 사용할 것이므로 고정 기능 파이프라인에서 구현된 투영 매핑을 HLSL로 변환해 보는 것이 중요합니다.

모델 좌표계에 존재하는 정점을 화면에 연출하기 위해서 우리는 3D 장면을 구성하는 월드의 행렬을 적용한 월드 변환, 카메라의 공간으로 변환하는 뷰 변환, 그리고 정규 또는 투영 변환을 작성해야 했습니다. 투영 매핑은 장면의 뷰, 투영 행렬 대신 조명의 위치와 방향으로 구성된 조명의 뷰 행렬과 투영 행렬을 사용하고 이 변환을 거친 위치를 텍스처 좌표로 사용하는 것이며 이 과정을 위해서 다음과 같이 최소한 5개의 행렬이 필요합니다.

 

float4x4       m_mtWld;       // 월드 변환 행렬

float4x4       m_mtViw;       // 카메라 뷰 행렬

float4x4       m_mtPrj;       // 3D 장면의 투영 행렬

float4x4       m_mtSdV;       // 조명의 위치와 방향으로 만든 뷰 행렬

float4x4       m_mtSdP;       // 조명의 투영 변환 행렬

 

속도의 향상을 위해서 월드 변환 행렬 * 카메라 뷰 행렬 * 3D 장면의 투영 행렬을 미리 곱한 행렬과 월드 변환 행렬 * 조명의 뷰 행렬  * 조명의 투영 행렬을 곱한 2개의 행렬을 쉐이더로 전달할 수도 있지만 여기는 구현의 내용에 초점을 두었기 때문에 5개의 행렬을 사용하고 있습니다.

 

정점 쉐이더 함수는 정점의 위치를 입력 받고, 이 위치를 가지고 출력 위치와 텍스처 좌표를 만듭니다.

 

void VtxPrc(in float4 iPos : POSITION0       // 입력: 정점의 위치

        ,  out float4 oPos : POSITION0 // 출력: 변환된 정점의 위치

        ,  out float2 oTex : TEXCOORD0 // 출력: 텍스처 좌표

        PosW = mul(iPos, m_mtWld);

        PosT = mul(PosW, m_mtViw);    // 3D 장면에 대한 뷰 변환

        PosT = mul(PosT, m_mtPrj);    // 3D 장면에 대한 투영 변환

        oPos = PosT;                  // 출력 위치 설정

 

이곳까지의 HLSL 코드는 정점의 월드, , 투영 변환에 해당합니다. 다음으로 텍스처의 매핑에 사용되는 UV 좌표를 설정하는 단계인데 장면 연출과 동일하게 월드 변환을 진행하고 다음으로 조명에 의존하는 뷰와 투영 행렬 변환을 수행합니다.

 

        PosT = mul(PosW, m_mtSdV);    // 조명에 대한 뷰 변환

        PosT = mul(PosT, m_mtSdP);    // 조명에 대한 투영 변환

 

3D에서 그래픽 파이프라인의 정규 또는 투영 변환을 거치면 x, y 값은 [-1, 1] 범위의 값으로 정규화 됩니다. 그런데 텍스처 좌표는 [0, 1] 이므로 정규 변환을 통과한 x 값은 [0, 1], y 값은 [1, 0] 범위로 조정해야 합니다. 조정된 값을 텍스처 좌표로 출력 레지스터에 쓰기만 하면 정점의 위치를 깊이 버퍼 그림자의 텍스처 좌표로 만드는 과정이 끝나게 됩니다.

 

        PosT.x = (1.0 + PosT.x) * 0.5F; // x 위치 [-1,1]에서 [0,1]로 변환

        PosT.y = (1.0 - PosT.y) * 0.5F; // y 위치 [-1,1]에서 [1,0]로 변환

        oTex = PosT;                  // 텍스처 좌표 설정

 

이 과정도 행렬을 사용할 때도 있습니다. 이 때 여러분은 다음과 같이 행렬을 만들어서 PotT에 곱해야 합니다.

 

D3DXMATRIX mtTex(0.5F0.0F, 0.0F, 0.0F,

                0.0F, -0.5F, 0.0F, 0.0F,

                0.0F0.0F, 1.0F, 0.0F,

                0.5F0.5F, 0.0F, 1.0F  );

 

텍스처 좌표가 [0, 1] 넘어서는 값들은 그림자를 적용할 필요가 없음으로 샘플러의 어드레스 모드를 Border 또는 Clamp로 설정할 수 있지만 Border로 설정하는 것이 좋고 Border 색상을 0xFFFFFFFF로 정합니다. 쉐이더의 색상은 1.0 이고 정규화된 깊이는 최대 1.0 이기 때문에 0xFFFFFFFF 값은 깊이 값을 1로 설정하는 것과 같은 의미입니다.

 

sampler SmpDif : register(s0) = sampler_state

        AddressU       = Border;

        AddressV       = Border;

        BorderColor    = 0xFFFFFFFF;

};

 

<이미지 투영: 정점 위치를 텍스처 좌표계로 사용한 예. ht26_shadow0.zip>

 

텍스처를 깊이 버퍼 그림자 매핑을 만들어 보았습니다. 이제 깊이 버퍼 그림자의 첫 번째 단계인 깊이 값 저장을 구현해 보겠습니다.

게임에서 깊이는 최소한 16비트 이상을 사용합니다. 따라서 이 정도의 깊이를 저장할 수 있는 해상도가 높은 텍스처를 사용해야 하는데 R8G8B8 형식의 텍스처보다 R16G16B16, R32G32B32 형식의 텍스처를 선택하거나 아니면 단일 색상으로 32비트 정보를 저장할 수 있는 D3DFMT_D32F 형식의 텍스처를 깊이 텍스처로 선택하는 것이 중요합니다. 그리고 텍스처를 생성하기 위해서 여러분은 D3D Device의 멤버 함수 CreateTexture() 함수를 사용하는 것보다 D3DXCreateTexture() 함수를 사용하는 것이 안전합니다.

 

D3DXCreateTexture(…, 1, D3DUSAGE_RENDERTARGET, D3DFMT_R32F, …);

 

쉐이더 코드는 정점의 위치를 조명에 대한 뷰 행렬과 투영 행렬을 적용해서 1차원 텍스처 좌표로 출력 합니다.

 

void VtxShadowMap( in float4 iPos : POSITION0

               , out float4 oPos : POSITION0

               , out float  oTex : TEXCOORD0

        Pos  = mul(iPos, m_mtWld);

        Pos  = mul(Pos,  m_mtSdV);    // 조명에 대한 변환

        Pos  = mul(Pos,  m_mtSdP);    // 조명에 대한 투영 변환

        oPos = Pos;

        oTex = Pos.z/Pos.w;

}

 

HLSL 코드는 3D 장면 연출에서 사용되는 변환 과정과 동일하고 단지 차이라면 마지막 줄에서 텍스처 좌표를 변환 된 위치의 "z"값을 "w"값으로 나누는 것입니다. 이렇게 하면 텍스처의 좌표는 [0, 1] 값으로 정규화 됩니다. 때로는 정규화 시키지 않고 그대로 픽셀 쉐이더로 넘기는 것도 생각할 수 있지만 다른 작업과 협업을 생각하면 정규화 하는 것이 좋습니다.

이렇게 정점에서 처리한 1차원 깊이 값을 픽셀 쉐이더 함수는 전달 받아서 이 1차원 좌표 값을 색상으로 그대로 출력합니다.

 

float4 PxlShadowMap(float Tex : TEXCOORD0) : COLOR0

{

        return Tex;

}

 

만약 여러분이 TargetR8G8B8 형식을 사용했다면 이 부분에서 깊이 값들이 유실 될 수 있어서 그림자 처리를 제대로 수행 못할 수 있게 됩니다. 또한 정점 처리 함수에서 변환된 깊이 값의 Semantic "COLOR#"으로 설정하지 않고 "TEXCOORD#"를 사용한 것은 특정 그래픽 카드는 "COLOR#" Semantic을 설정하면 정점 처리 후에 픽셀 단계로 전달 할 때 [0, 1] 범위로 정규화 하는 것도 있기 때문입니다.

 

이렇게 조명에서 바라본 정점의 최종 깊이 값을 파이프라인과 쉐이더를 사용해서 만들었습니다. 다음 단계는 최종 깊이 값과 현재의 깊이 값을 비교해서 그림자를 적용할 차례입니다. 픽셀 처리로 투영 매핑 좌표와 깊이 값을 저장할 수 있는 구조체를 선언 합니다.

 

struct VsOut

{

        float4 Pos : POSITION0;

        float4 Dif : TEXCOORD1;               // Diffuse 색상

        float2 Shd : TEXCOORD2;               // 그림자 UV

        float  Dpc : TEXCOORD3;               // 조명에서 바라본 현재의 깊이 값

};

 

앞서 현재의 깊이 값과 최종 깊이 값의 비교를 위해서 정점 처리 함수에서 현재의 깊이 값을 계산한다고 했습니다. 현재의 깊이 값은 조명의 뷰와 투영 행렬로 결정을 해야 합니다.

 

VsOut VtxShadowScene(float4 iPos : POSITION0, float3 iNor : NORMAL0)

        PosW = mul(iPos, m_mtWld);    // 위치의 월드 변환

        PosT = mul(PosW, m_mtSdV);    // 조명에 대한 뷰 변환

        PosT = mul(PosT, m_mtSdP);    // 조명에 대한 투영 변환

        Out.Dpc = (PosT.z-0.01)/PosT.w;       // shift: z-bias

 

        PosT.x = (1.0 + PosT.x) * 0.5; // 투영 텍스처 좌표 X

        PosT.y = (1.0 - PosT.y) * 0.5; // 투영 텍스처 좌표 Y

        Out.Shd = PosT;                       // 투영 텍스처 좌표 출력

 

정점의 투영 변환 후에 "-0.01"를 더한 것은 픽셀 처리에서 텍스처에 저장된 최종 깊이 값과 비교를 할 때 비교 오차로 인해서 줄 무늬가 나타날 수 있기 때문입니다. 이를 위해서 z 값을 적당히 이동 시키는 z-bias가 필요하며 이 값을 대충 "-0.01"로 설정한 것입니다. 정교한 프로그램이라면 이 부분도 깊이 값에 의존하도록 작성해야 됩니다.

 

픽셀 처리 함수는 정점 투영 텍스처 좌표를 가지고 최종 깊이를 저장한 텍스처에서 깊이 값을 가져옵니다. 다음으로 이 값을 정점 처리에서 전달된 깊이 값과 비교를 합니다. 만약 정점 처리에서 전달된 값이 텍스처에서 추출한 값보다 크면 그림자를 적용할 픽셀로 결정이 됩니다. 다음 쉐이더 코드에서는 이 값을 0.0으로 정했습니다.

 

float4 PxlShadowScene(VsOut In) : COLOR0

{

        float4  Out = 0;       // 투영 텍스처 좌표 출력

        float   Shd = 0;       // 그림자 유무

 

        // 텍스처에서 조명에서 바라본 최종 깊이 값 추출

        Shd = tex2D(SmpShd, In.Shd);

 

        // 정점 처리에서 만든 깊이 값과 비교

        // 정점 처리의 값보다 작으면 그림자를 적용할 대상으로

        // 색상을 0으로 함

        if(Shd >= In.Dpc)

               Shd = 1.0;

        else

               Shd = 0.0;

 

        Out  = Shd * In.Dif;

 

<깊이 버퍼 그림자. ht26_shadow1.zip>

 

그림자가 적용된 부분을 확대하면 Aliasing을 볼 수 있습니다. 좀 더 부드러운 그림자를 만들기 위해서 픽셀 처리에서 9-CON Sampling 등으로 간단하게 해결 할 수도 있습니다. 9-CON 샘플링은 현재의 픽셀과 인접한 8개의 픽셀을 혼합하는 방법입니다. 계산을 빨리 하기 위해서 2차원 좌표로 구성된 9개의 좌표가 필요합니다. 다음의 9-CON 샘플링 테이블은 깊이 버퍼 그림자 텍스처의 사이즈가 1024x1024이기 때문에 인접한 픽셀을 얻기 위해서 1/1024.0 값을 사용하고 있습니다.

 

// 9-CON 샘플링 테이블

static float2 c[9]=

{

{-1./1024., -1./1024.}, {-1./1024., 0./1024.}, {-1./1024., 1./1024.},

{ 0./1024., -1./1024.}, { 0./1024., 0./1024.}, { 0./1024., 1./1024.},

{ 1./1024., -1./1024.}, { 1./1024., 0./1024.}, { 1./1024., 1./1024.},

};

 

9-CON 샘플링의 적용은 9번 샘플링 해서 이 결과를 가지고 누적시켜서 그림자 적용을 결정합니다.

 

float4 PxlPrc(VsOut In, uniform bool bTex=true) : COLOR0

{

        float4 Out = 0;

        float  Shd = 0;

        float  r = 0;

 

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

        {

               r = tex2D(SmpShd, In.Shd + c[i]);

               if(r >= In.Dpc)

                       Shd += 1.0;

        }

 

        Shd *= 0.1111f;

        Out = Shd * In.Dif;

 

<9-CON 샘플링이 적용된 깊이 버퍼 그림자. ht26_shadow2.zip>



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

Creative Commons License