법선맵핑
텍스쳐의 텍셀(텍스쳐 엘리먼트 : 텍스쳐의 일부)에 RGB색깔값이 아닌 법선 벡터값이 들어있는 텍스쳐를 사용하여 특수하게 맵핑하는 기법이다.
이 텍스쳐의 법선벡터와 광원의 연산을 통해서 정점 단위의 조명이 아닌 픽셀 단위의 조명을 할 수 있도록 한다. 여기서는 픽셀별 조명 기법의 대표적인 예를 가지고 해보자


법선맵핑의 종류
법선벡터의 기준좌표계에 따라서 오브젝트 공간 법선 맵과 접선공간 법선 맵으로 나뉜다.

오브젝트 공간 법선 맵 : 법선벡터가 기준으로 삼는 좌표계가 맵이 생성되는 3D오브젝트의 공간 좌표계가 기준이 되는 맵이야, 오브젝트의 원점이 법선벡터의 원점이 되는거지, 가장 직관적이구 어지간한 하드웨어가 다 지원하는 것이다

접선공간 법선 맵 : 모든 법선 벡터를 각 정점의 접선공간으로 변환하여 보관하는 방식, 이렇게 하면 모든 법선 벡터는 값이 항상 1.0인 벡터로 변환된다. 그래서 이렇게 만들어진 법선 맵은 값이 항상 1.0이야, 그러니까 RGB중에서 B값이 255이므로, 전체적으로 푸른색을 띄게된다. 직접적인 API가 존재하지 않고, 정점이나 픽셀셰이더로 직접 만들어야한다




벡터 -> RGB
예를들어, 단위벡터 v=(x,y,z)가 있을 때, 이를 RGB값으로 변환시켜보자
변환방법에는 여러가지가 있지만, 여기서는 ARGB를 각각 8비트씩 변환시켜보도록 하겠다.
그렇게되면 ARGB가 각각 8비트씩 할당되어 총 32비트 DWORD가 된다.

v=(x,y,z)는 단위벡터이므로 x,y,z 값은 각각 -1.0 ~ 1.0 사이가 된다.
그리고 우리가 변환하려는 결과값의 범위는 8비트이므로 0~255사이의 값이다.
즉, (-1.0 = 0, +1.0 = 255)가 되는 함수를 만들면 되는데 그건 아래와 같다

R = (DWORD)(127.0*x + 128.0)
G = (DWORD)(127.0*y + 128.0)
B = (DWORD)(127.0*z + 128.0)

이를 DWORD형태의 ARGB로 만들라면 아래와같이......

Color = ((R<<16) | (G<<8) | B)

어허
이렇게 얻어진 컬러값에는 벡터 v=(x,y,z)의 정보가 RGB값으로 변환되어 들어가 있는 것
여기서 만약 접선공간 법선 맵을 사용한다면 z값이 항상 1.0이므로, B값을 생략할 수 있어, 그리구 ARGB중에서 A값도 생략한다면, 최종적으로 RG값만 남는건데, 이를 이용하면 R이랑 G를 각각 16비트씩 확장시킬 수 있다.
32비트 ARGB(8:8:8:8)요고를 RG(16:16)로 변환하는거지. 이렇게 비트가 높아지면 더 정밀한 법선맵핑을 구현하는게 가능하다


법선맵핑의 효과
법선맵핑은 일반적으로 저폴리곤 모델을 더 높은 수준의 고폴리곤처럼 보여줄 때 사용되는 기술이다.
법선 맵과 텍셀에 들어있는 RGB값은 색깔을 나타내는것이 아니라 맵핑되는 지점의 픽셀에서의 법선 벡터(x,y,z)값을 나타낸다. 일반적으로 1000개 정도의 폴리곤에 법선 맵을 적용할 경우 20000~30000개의 폴리곤으로 모델링한 효과를 볼 수 있으며, 그 이상도 가능허다

저폴리곤 + 법선맵 = 고폴리곤


법선 맵의 생성
결국 저 폴리곤에 법선 맵을 적용하여, 마치 대용량 고폴리곤처럼 보여주는것이 법선 맵핑이라는 기법인데, 문제는 이 법선 맵을 어떻게 만들어 내는가!?

높이 맵에 의한 생성법
과거에는 주로 건물의 벽면이나 캐릭터의 갑옷 텍스쳐에 법선 맵을 사용하였는데, 이 경우에는 기본 텍스쳐를 흑백 이미지로 변환하여 높이맵으로 만든다
그리고 이 높이맵에서 텍셀간의 고저차를 비교하여 미분 벡터를 만든다. 
이렇게 만들어진 미분 벡터들을 RGB로 변환하여 텍셀에 저장하면 되는 것이다..

그리는데 삼만년걸렸습니다

그림을 보면 인접한 픽셀들의 높이차를 비교하여 x방향 미분벡터 Vx랑 y방향의 미분벡터 Vy를 만들어, 이렇게 만들어진 2개의 벡터를 외적하면 직교 벡터 N이 만들어지는데 이게 바로 법선벡터가 되는것이다.


D3D에서는 D3DXComputeNormalMap()이라는 함수가 높이 맵에서 법선 맵을 만들어주는 함수다...... ㅇㅋ


오브젝트 비교에 의한 생성법

법선 맵을 생성하는 방법에는 간단하게 텍스쳐의 높이 맵을 사용하여 생성하는 방법과 오브젝트를 저폴리곤과 고폴리곤 2개로 만든 뒤 두 모델의 차이를 비교하여 법선 맵을 생성하는 방법이 있다. 최근에는 후자의 방법을 주로 사용한다.


이 방법의 원리는 저폴리곤의 텍셀 위치에서 반직선을 쏴서 고폴리곤과 접하는 지점의 법선 벡터값을 저장하는것이다. 중간값을 얻어내는 것..



법선 맵 만들기

법선 맵 생성도구를 직접 만드는것도 가능하겠지만, 여기서는 다른 사람들이 잘 만들어둔 도구를 사용하자,, 


ATI의 NormalMapper

http://www.ati.com/developer

요기가면 있다, NormalMapperUI라는 3DS 맥스용 도구가 공개되어있어, 요고는 맥스스크립트로 작성되어있고 확장자는 mzp로 되어있다



디스크릿의 NormalRender

맥스를 제작한 디스크릿에서는 NormalRender라는 플러그인을 공개했다.

이를 이용하면 법선 맵을 손쉽게 제작할 수 있다.




실제예제

 - 높이 맵 기법

D3D의 내적법선 맵핑 기능은 API수준에서 구현되어있기 때문에 사용하기 쉽다.

우리는 그저 적절한 텍스쳐를 텍스쳐 스테이지에 올려주고 내적법선맵핑기능을 사용해 텍스쳐 맵핑을 하도록 지시하기만 하면 되기 때문이야^^



중요부위만 빠르게 훑고 지나간다!


#include <d3d9.h>

#include <d3dx9.h>


#define WINDOW_W 500

#define WINDOW_H 500


HWND g_hwnd;

LPDIRECT3D9 g_pD3D = NULL;

LPDIRECT3DDEVICE9 g_pd3dDevice = NULL;

LPDIRECT3DVERTEXBUFFER9 g_pVB = NULL;


// 법선 맵을 사용하기 위해서 텍스쳐 인터페이스 선언!

LPDIRECT3DTEXTURE9 g_pTexDiffuse = NULL;

LPDIRECT3DTEXTURE9 g_pTexNormal = NULL;


D3DXMATRIXA16 g_matAni;

D3DXVECTOR3 g_vLight;


struct CUSTOMVERTEX

{

FLOAT x, y, z;

DWORD color;

FLOAT u, v;

};


#define D3DFVF_CUSTOMVERTEX (D3DFVF_XYZ|D3DFVF_DIFFUSE|D3DFVF_TEX1|D3DFVF_TEXCOORDSIZE2(0))


HRESULT InitD3D( HWND hWnd )

{

    /// 디바이스를 생성하기위한 D3D객체 생성

    if( NULL == ( g_pD3D = Direct3DCreate9( D3D_SDK_VERSION ) ) )

        return E_FAIL;


    D3DPRESENT_PARAMETERS d3dpp;                /// 디바이스 생성을 위한 구조체

    ZeroMemory( &d3dpp, sizeof(d3dpp) );        /// 반드시 ZeroMemory()함수로 미리 구조체를 깨끗이 지워야 한다.

    d3dpp.Windowed = TRUE;                      /// 창모드로 생성

    d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;   /// 가장 효율적인 SWAP효과

    d3dpp.BackBufferFormat = D3DFMT_UNKNOWN;    /// 현재 바탕화면 모드에 맞춰서 후면버퍼를 생성

d3dpp.AutoDepthStencilFormat = D3DFMT_D16; /// 복잡한 오브젝트를 그릴것이라 Z버퍼가 필요하다

d3dpp.EnableAutoDepthStencil = TRUE;


    /// 디바이스를 다음과 같은 설정으로 생성한다.

    if( FAILED( g_pD3D->CreateDevice( D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWnd,

                                      D3DCREATE_SOFTWARE_VERTEXPROCESSING,

                                      &d3dpp, &g_pd3dDevice ) ) )

    {

        return E_FAIL;

    }


g_pd3dDevice->SetRenderState(D3DRS_CULLMODE, D3DCULL_NONE);

/// Z버퍼를 켠다

g_pd3dDevice->SetRenderState(D3DRS_ZENABLE, TRUE);


g_pd3dDevice->SetRenderState(D3DRS_LIGHTING, FALSE);


    return S_OK;

}


HRESULT InitVB()

{

CUSTOMVERTEX vertices[] = {

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

{ 1, 1, 0, 0xffffffff, 1, 0},

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

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

};


if(FAILED(g_pd3dDevice->CreateVertexBuffer(4*sizeof(CUSTOMVERTEX), 0, D3DFVF_CUSTOMVERTEX,

D3DPOOL_DEFAULT, &g_pVB, NULL)))

{

return E_FAIL;

}


VOID* pVertices;

if(FAILED(g_pVB->Lock(0, sizeof(vertices), (void**)&pVertices, 0)))

return E_FAIL;

memcpy(pVertices, vertices, sizeof(vertices));

g_pVB->Unlock();


return S_OK;

}


HRESULT InitTexture()

{

if(FAILED(D3DXCreateTextureFromFile(g_pd3dDevice, "env2.bmp", &g_pTexDiffuse)))

return E_FAIL;


if(FAILED(D3DXCreateTextureFromFile(g_pd3dDevice, "normal.bmp", &g_pTexNormal)))

return E_FAIL;


return S_OK;

}


HRESULT InitGeometry()

{

if(FAILED(InitVB())) return E_FAIL;

if(FAILED(InitTexture())) return E_FAIL;


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


return S_OK;

}


void SetupCamera()

{

D3DXMATRIXA16 matWorld;

D3DXMatrixIdentity(&matWorld);

g_pd3dDevice->SetTransform(D3DTS_WORLD, &matWorld);


D3DXVECTOR3 vEyePt(0.0f, 0.0f, -4.0f);

D3DXVECTOR3 vLookatPt(0.0f, 0.0f, 0.0f);

D3DXVECTOR3 vUpVec(0.0f, 1.0f, 0.0f);

D3DXMATRIXA16 matView;

D3DXMatrixLookAtLH(&matView, &vEyePt, &vLookatPt, &vUpVec);


g_pd3dDevice->SetTransform(D3DTS_VIEW, &matView);


D3DXMATRIXA16 matProj;

D3DXMatrixPerspectiveFovLH(&matProj, D3DX_PI/4, 1.0f, 1.0f, 100.0f);

g_pd3dDevice->SetTransform(D3DTS_PROJECTION, &matProj);

}


// 사용자의 마우스 커서 위치가 광원의 위치가 되도록 하자

VOID Animate()

{

D3DXMatrixIdentity(&g_matAni);


POINT pt;

GetCursorPos(&pt);                         // 커서의 스크린 좌표

ScreenToClient(g_hwnd, &pt);          // 스크린좌표를 클라이언트 좌표계로 바꾼다


g_vLight.x = -(((2.0f*pt.x)/WINDOW_W)-1);    // 커서의 좌표값을 -1 ~ +1 사이의 값으로 한다

g_vLight.y = -(((2.0f*pt.y)/WINDOW_H)-1);

g_vLight.z = 0.0f;


if(D3DXVec3Length(&g_vLight)>1.0f)

D3DXVec3Normalize(&g_vLight, &g_vLight);     // 벡터의 정규화

else

g_vLight.z = sqrtf(1.0f - g_vLight.x*g_vLight.x - g_vLight.y*g_vLight.y);

}

요렇게 얻어진 좌표를 Render()함수에서 텍스쳐값으로 등록한다


VOID CleanUp()

{

if(g_pVB != NULL)

g_pVB->Release();


if(g_pd3dDevice != NULL)

g_pd3dDevice->Release();


if(g_pD3D != NULL)

g_pD3D->Release();

}


void DrawMesh(D3DXMATRIXA16* pMat)

{

g_pd3dDevice->SetTransform(D3DTS_WORLD, pMat);

g_pd3dDevice->SetStreamSource(0, g_pVB, 0, sizeof(CUSTOMVERTEX));

g_pd3dDevice->SetFVF(D3DFVF_CUSTOMVERTEX);

g_pd3dDevice->DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2);

}


DWORD VectortoRGBA(D3DXVECTOR3* v, FLOAT fHeight)

{

DWORD r = (DWORD)(127.0f * v->x + 128.0f);

DWORD g = (DWORD)(127.0f * v->y + 128.0f);

DWORD b = (DWORD)(127.0f * v->z + 128.0f);

DWORD a = (DWORD)(255.0f * fHeight);


return((a<<24L) + (r<<16L) + (g<<8L) + (b<<0L));

}


VOID Render()

{

D3DXMATRIXA16 matWorld;

    /// 후면버퍼와 Z버퍼를 지운다

    g_pd3dDevice->Clear( 0, NULL, D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER, D3DCOLOR_XRGB(0,0,255), 1.0f, 0 );

    

Animate();

    /// 렌더링 시작

    if( SUCCEEDED( g_pd3dDevice->BeginScene() ) )

    {

g_pd3dDevice->SetTexture(0, g_pTexNormal);

g_pd3dDevice->SetTexture(1, g_pTexDiffuse);


g_pd3dDevice->SetSamplerState(0, D3DSAMP_MAGFILTER,

D3DTEXF_LINEAR);

g_pd3dDevice->SetSamplerState(1, D3DSAMP_MAGFILTER,

D3DTEXF_LINEAR);

g_pd3dDevice->SetTextureStageState(0, D3DTSS_TEXCOORDINDEX, 0);

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

DWORD dwFactor = VectortoRGBA(&g_vLight, 0.0f);  // 벡터를 RGB로

g_pd3dDevice->SetRenderState(D3DRS_TEXTUREFACTOR, dwFactor);  // RGB로 변환된 벡터를 TextureFactor값으로 등록

요러고나서 아래와같이 텍스쳐와 텍스쳐펙터로 등록된 광원을 내적연산시킨다!

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

g_pd3dDevice->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_DOTPRODUCT3);

g_pd3dDevice->SetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_TFACTOR);


g_pd3dDevice->SetTextureStageState(1, D3DTSS_COLOROP, D3DTOP_MULTIPLYADD);

g_pd3dDevice->SetTextureStageState(1, D3DTSS_COLORARG1, D3DTA_TEXTURE);

g_pd3dDevice->SetTextureStageState(1, D3DTSS_COLORARG2, D3DTA_CURRENT);

g_pd3dDevice->SetTextureStageState(1, D3DTSS_ALPHAOP, D3DTOP_SELECTARG1);

g_pd3dDevice->SetTextureStageState(1, D3DTSS_ALPHAARG1, D3DTA_TEXTURE);


DrawMesh(&g_matAni);

g_pd3dDevice->EndScene();

}


    /// 후면버퍼를 보이는 화면으로!

    g_pd3dDevice->Present( NULL, NULL, NULL, NULL );

}


LRESULT WINAPI MsgProc( HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam )

{

    switch( msg )

    {

        case WM_DESTROY:

            CleanUp();

            PostQuitMessage( 0 );

            return 0;

    }


    return DefWindowProc( hWnd, msg, wParam, lParam );

}


INT WINAPI WinMain( HINSTANCE hInst, HINSTANCE, LPSTR, INT )

{

    /// 윈도우 클래스 등록

    WNDCLASSEX wc = { sizeof(WNDCLASSEX), CS_CLASSDC, MsgProc, 0L, 0L, 

                      GetModuleHandle(NULL), NULL, NULL, NULL, NULL,

                      "BasicFrame", NULL };

    RegisterClassEx( &wc );


    /// 윈도우 생성

    HWND hWnd = CreateWindow( "BasicFrame", "LightMapping", 

                              WS_OVERLAPPEDWINDOW, 100, 100, WINDOW_W, WINDOW_H,

                              GetDesktopWindow(), NULL, wc.hInstance, NULL );

g_hwnd = hWnd;


    /// Direct3D 초기화

    if( SUCCEEDED( InitD3D( hWnd ) ) )

    { 

/// 정점버퍼 초기화

if(SUCCEEDED(InitGeometry())){


SetupCamera();


/// 윈도우 출력

ShowWindow( hWnd, SW_SHOWDEFAULT );

UpdateWindow( hWnd );


/// 메시지 루프

MSG msg; 

ZeroMemory(&msg, sizeof(msg));


while(msg.message!=WM_QUIT)

{

if(PeekMessage(&msg, NULL, 0U, 0U, PM_REMOVE)){

TranslateMessage( &msg );

DispatchMessage( &msg );

}

else

Render();

}

}

    }


    /// 등록된 클래스 소거

    UnregisterClass( "D3D Tutorial", wc.hInstance );

    return 0;

}

 

요렇게 두개 합친것이다^^

 




'Graphics > DirectX' 카테고리의 다른 글

Direct X - 애니메이션  (0) 2014.07.29
Direct X - 계층구조  (3) 2014.07.28
Direct 3D - 빌보드  (0) 2014.07.24
Direct 3D - 라이트 맵핑  (0) 2014.07.23
Direct 3D - 텍스쳐  (0) 2014.07.21
Posted by 긍정왕오킹