델리게이트란 무엇인가?

델리게이트는 사전적 의미로 위임하다라는 뜻입니다. Cocoa Touch에서 델리게이트는 다른 객체를 대신해 특정 작업을 수행하는 클래스를 말합니다. 다시말해 한 오브젝트가 모든 기능을 수행하지 않고, 일부 기능을 다른 객체에게 위임하는 객체 지향 프로그래밍 디자인 패턴을 뜻합니다. 그리고 그 도움을 주는 객체를 델리게이트라고 부릅니다.

Objective-C 프로그래밍에 있어서 델리게이트는 매우 중요한 요소라고 할 수 있습니다. 이 델리게이트가 어플리케이션의 생명주기와도 갚은 연관이 있기 때문인데, 델리게이트가 없다면 생명을 잃어버린 애플리케이션이라고 말 할 수 있습니다.


애플리케이션의 생명주기

어떤 프로그램이든 소스 코드를 통하여 메모리에 적재되고, 실행되는 동안 주어진 역할을 수행하고, 임무를 완수할 경우에는 메모리에서 삭제되는 새명주기 싸이클이 존재합니다. 아이폰의 어플리케이션도 이러한 패턴을 무시할 수 없습니다. 아래의 그림은 아이폰 어플리케이션의 생명주기를 나타낸 것입니다.

어플리케이션의 생명주기는 실행과 종료 사이에 발생하는 순차적인 이벤트들을 만들어냅니다. 사용자는 아이폰의 스크린의 아이콘을 선택하여 어플리케이션을 실행하게 됩니다. 이후에 시스템은 진행상태를 보여주고 어플리케이션의 main함수를 호출합니다. 이때부터 초기화를 위한 작업들이 UIKit으로 넘어오는데, 어플리케이션의 UI를 불러오고 이벤트 루프를 준비하는 작업들이 포함됩니다. 이벤트 루프 과정에서 UIKit은 커스텀 객체로의 이벤트 전달과 어플리케이션에서 발생한 명령들을 처리하는 것을 조절하게 됩니다. 사용자가 프로그램을 종료하는 이벤트를 발생하면, UIKit을 통해서 어플리케이션에 종료명령을 발생하고 종료가 진행됩니다.


어플리케이션 델리게이트

어플리케이션 델리게이트(Application Delegate) 객체의 역할은 어플리케이션의 동작을 모니터링 하는 것입니다. 변수의 경우, 각 변수가 가지고 있는 값들을 어떤 특정한 패턴, 함수에 의해 변형되어, 개발자의 의도대로 결과 값을 얻을 수 있습니다. 이런 특징적인 역할을 하는 함수(모듈, 객체, 메소드 등)들을 특정한 조건이나 의도를 가지고 묶음으로 만들어내어 보다 간편하게 사용하도록 하는 것이 델리게이트를 사용하는 목적이라고 할 수 있습니다. 또 한가지 델리게이트를 사용하는 이유는 UIApplication과 같은 복잡한 객체를 상속하는 것을 피하고, 메소드를 재정의 하지 않음으로써 보다 객체지향적인 프로그래밍을 할 수 있도록 도와주게 됩니다. 아래의 그림은 객체가 델리게이트를 통해 특정 액션을 실행하는 과정을 표현한 것 입니다.


어플리케이션이 어떤한 액션을 만

/*************************************************************************/

/* Flip

/*************************************************************************/

LPDIRECTDRAWSURFACE7 m_pDDSPrimary;

LPDIRECTDRAWSURFACE7 m_pDDSBack;

LPDIRECTDRAWSURFACE7 m_pDDSOffScreen;

DDSURFACEDESC2 m_ddsd;

.....

 

/////////////////////////////////////////////////////////////////////////////////////////////////

//프리아머리 서페이스와, 백 서페이스, 오프 스크린 생성

ZeroMemory( &m_ddsd, sizeof(m_ddsd) );
m_ddsd.dwSize = sizeof(m_ddsd);
m_ddsd.dwFlags = DDSD_CAPS | DDSD_BACKBUFFERCOUNT;
m_ddsd.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE | DDSCAPS_FLIP | DDSCAPS_COMPLEX;
m_ddsd.dwBackBufferCount = 1;

 

if (FAILED(m_lpdd->CreateSurface(&m_ddsd, &m_pDDSPrimary, NULL)))
{
  MessageBox( NULL, "CreateSurface 실패했습니다.", "에러", MB_OK );

  return FALSE;
}

 

DDSCAPS2 ddscaps;

ZeroMemory( &ddscaps, sizeof(ddscaps) );

ddscaps.dwCaps = DDSCAPS_BACKBUFFER;

 

if( FAILED(m_pDDSPrimary->GetAttachedSurface(&ddscaps, &m_pDDSBack)) )
{
    MessageBox( NULL, "GetAttachedSurface 실패했습니다.", "에러", MB_OK );
  
    return FALSE;
}

 

ZeroMemory( &m_ddsd, sizeof(m_ddsd) );
m_ddsd.dwSize = sizeof(m_ddsd);
m_ddsd.dwFlags = DDSD_CAPS | DDSD_WIDTH | DDSD_HEIGHT;
m_ddsd.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN;
m_ddsd.dwWidth = 640;
m_ddsd.dwHeight = 480;

 

if (FAILED(m_lpdd->CreateSurface(&m_ddsd, &m_pDDSOffScreen, NULL)))
{
    MessageBox( NULL, "CreateSurface 실패했습니다.", "에러", MB_OK );

    return FALSE;

/////////////////////////////////////////////////////////////////////////////////////////////////

.....

 

m_pDDSBack->Blt( NULL, g_pGame->m_pDDSOffScreen, NULL, DDBLTFAST_WAIT, NULL );

m_pDDSPrimary->Flip( NULL, DDFLIP_WAIT );

LPDIRECTDRAWSURFACE7 m_pDDSOffScreen;

DDSURFACEDESC2 ddsd;

 

///////////////////////////////////////////////////////////////////////////

ZeroMemory( &ddsd, sizeof(m_ddsd) );
ddsd.dwSize = sizeof(m_ddsd);
ddsd.dwFlags = DDSD_CAPS DDSD_WIDTH | DDSD_HEIGHT;
ddsd.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN;
ddsd.dwWidth = 640;
ddsd.dwHeight = 480;

 

if (FAILED(m_lpdd->CreateSurface(&m_ddsd, &m_pDDSOffScreen, NULL)))
{
    MessageBox( NULL, "CreateSurface 실패했습니다.", "에러", MB_OK );

    return;

//////////////////////////////////////////////////////////////////////////

밉맵은 하나의 텍스쳐를 일정한 크기로 축소시켜서 여러장을 가지고 그려질때 현재 사이즈에

가장 적합한 이미지를 찾아서 쓰는 것을 말한다.

이것의 이점을 본다면, 일단 기본적으로 폴리곤에 텍스쳐를 입힐때 폴리곤에 맞는 텍셀을 찾아야 

한다.  따라서 폴리곤이 화면에 보일때 텍스쳐 실제 사이즈와 동일하다면, 내부적으로 매우 쉽게

찾을 것이다. 하지만 그렇지 못할때, 적당한 값을 찾아서 그리는데 시간을 소모할 것이다. 
( 보통 필터링을 사용해서 색보간을 해서 그린다. 작게 할때는 색을 합치고 키울때는 보간하고...)
문제는 실제 화면에 한픽셀을 차지하는 폴리곤이 있다고 했을때 그 한픽셀을 그리기 위해서

512 X 512 크기의 텍스쳐를 모두 필터링해서 하나의 픽셀을 가져온다고 생각해보자.
생각만으로도 느릴것같다는 생각이 들지 않는가? ( 아니라고 말하는 당신. 그냥 그렇다고 생각해주시구려... ) 그래서 나온것이 바로 밉맵이다.


실제로 밉맵은 구형 그래픽카드일수록 그 효과가 잘 나타나지만(속도라던지..), 최신카드일 수

록 잘 나타나지 않는다.
일단 자동으로 밉맵을 만들기위해서는 2의 지수형 정방형 텍스쳐이어야만한다.
밉맵을 쓰지않고 하나만 만들면 사이즈를 자유롭게 할수있지만, 그게 아니면 2의 지수형태를

띠어야만한다.

( OGL에서는 무조건적으로 2의지수형이어야만 만들어졌었다. 최신OGL은 바뀌었다는 이야기가

들리지만.. )

그렇다. 다 아는 내용을 이야기 했다. 사실 내가 하고싶은 말은 여기서 부터다. (라고해봐야...ㅡ.ㅡ)

기본적으로 다이렉트X에서 텍스쳐를 생성할때 밉맵체인 (그러니까 하나의 이미지를 축소시키는

단계를 말한다.)의 수를 정할 수 있다. 기본적으로 옵션에 그부분에 0을 넣는다.
0은 디폴트값을 의미하며, 자동으로 알아서 끝까지 만들어 준다는 의미다. 그래서 언제나 텍스쳐를

 로드할때 그냥 밉맵레벨에 0을 넣어서 자동으로 밉맵을 만들어서 사용하는 것이다.

문제는 생성한 텍스쳐를 가지고 장난을 칠때 종종 실수를 한다는 것.
일단 텍스쳐에 렌더링을 한다고 하자. 그때는 렌더텍스쳐를 생성하는데 이때도 그냥 아무 생각없이

 밉맵을 자동으로 생성해버리기 일수이다. 
뭐 그렇게 해서 생성이 안된다면 다행이지만(다행일까?), 분명한 것은 아무 문제없이 생성이 된다.
그다음에 우리는 0번표면에 렌더링을 걸고, 이후 그것을 폴리곤 어딘가에 발라서 사용할때, 문제가 발생한다. 아무것도 안그려졌거나, 괴상하게 깨져나가는 텍스쳐를 발견하게 될것이다.
물론 운이 좋아서, 아무 문제없이 잘 그려지기도 한다. 
 처음에 잘그려짐을 경험하고, 나중에 안됨을 경험하면 왜 안되는건지 파악하는데 제법시간이

걸린다. ( 그게바로 나다. ㅡ.ㅡ )

 

 밉맵체인은 텍스쳐를 로딩할때 자동으로 한번 만들어 주는것 뿐이지, 이후 표면을 수정했다고

자동으로 나머지들도 만들어주는 것은 아니라는 사실을 몰랐던 시절이다.

 이것은 오직 렌더텍스쳐만 말하는것이 아니다. 텍스쳐에 lock을 걸고 편집작업을 하는것도 역시

마찬가지다. 보통 0번만 편집하고 사용하는데, 역시 위와 같은 문제가 발생해서 왜 안되는건지

대책없이 멍하니 있거나 엄한 D3D와 Ms를 욕하는 일을 벌일 수 있다는 것이다.

 

그럼 이글을 읽은 초심자라던가, 아직 D3D라는 것을 해봤지만, 텍스쳐가지고 장난 안 친 사람들

이라면, 밉맵체인을 자동으로 만들고, 0번만 편집하고 똑바로 안된다고 시간허비하지 말기를...

" 알파합성 "

 

1. 알파합성이란

  - 폴리곤을 렌더링할 때, 폴리곤 색을 그냥 출력하는 것이 아니라 이미 렌더링된 화면에 여러 가지 합성을 해서 출력하는 방법

 

2. 주의 사항

  - 알파 합성은 현재 그려져 있는 색과 렌더링하는 폴리곤의 색을 합성한다. 따라서 반투명 폴리곤을 렌더링하기 저에, 불투명 오브젝트를 렌더링해야 한다.

 

3. 알파합성 사용법

  // 폴리곤을 반투명으로 렌더링할지 여부 결정

  m_pd3dDeviece->SetRenderState(D3DRS_ALPHABLENDANABLE, TRUE);

  m_pd3dDeviece->SetRenderState(D3DRS_ALPHABLENDANABLE, FALSE);

  // 어떻게 색을 조합할지

  m_pd3dDeviece->SetRenderState(D3dRS_BLENDOP, 값);

  // 폴리곤의 합성 강도

  m_pd3dDeviece->SetRenderState(D3DRS_SRCBLEND, 값A);

  m_pd3dDeviece->SetRenderState(D3DRS_DESTBLEND, 값B);

 

4. 다양한 합성 방법

  - 선형 합성

     : 최종색 = ( 1 - a ) * 바닥색 + a * 덮일색

     m_pd3dDeviece->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);

     m_pd3dDeviece->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);

  - 덧셈 합성

     : 최종색 = 바닥색 + a * 덮일색

     : 빛의 표현에 자주 사용

     m_pd3dDeviece->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_ONE);

     m_pd3dDeviece->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);

  - 뺄셈 합성

     : 최종색 = 바닥색 - a * 덮일색

     : 간단한 그림자 표현에 자주 사용

     m_pd3dDeviece->SetRenderState(D3DRS_BLENDOP, D3DBLENDOP_REVSUBTRACT);

     m_pd3dDeviece->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_ONE);

     m_pd3dDeviece->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);

  - 곱셈 합성

     : 한쪽이 검으면 최종색이 검게 된다.

     : 최종색 = 바닥색 * 덮일색

     m_pd3dDeviece->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_SRCCOLOR);

     m_pd3dDeviece->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_ZERO);

  - 제곱 합성

     : 어두운 곳은 더 어둡게 하고, 밝은 곳은 그대로 표시

     : 최종색 = 바닥색 * 바닥색

     m_pd3dDeviece->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_DESTCOLOP);

     m_pd3dDeviece->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_ZERO);

  - 네거티프 포지티프 반전

     : 흰 폴리곤을 렌더링하면, 검은 부분은 희게, 흰 부분은 검게 출력 되도록 색을 역전하는 변환

     : 최종색 = ( 1 - 바닥색) * 덮일색

     m_pd3dDeviece->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_ZERO);

     m_pd3dDeviece->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_INVDESTCOLOR);

  - 불투명

     : 최종색 = 덮일색

     m_pd3dDeviece->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_ZERO);

     m_pd3dDeviece->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_ONE);

stencil buffer 특수한 효과를 위한 off-screen buffer, back buffer depth buffer 동일한 해상도를 가진다. 따라서, stencil buffer 내의 (i, j)번째 픽셀은 back/depth buffer (i, j)번째 픽셀과 대응된다.

이름이 의미하는 처럼 stencil buffer back buffer 일정 부분이 렌더링되는 것을 막는 효과를 위해 사용된다. 예를 들어, 거울에 특정 물체를 반사하도록 할려고 한다면, 거울에 반사되는 부분에 대해서만 드로잉을 수행하면 된다. 거울에 비치지 않는 부분은 렌더링되는 것을 막을 있는 있도록 하는 것이 바로 stencil buffer.

stencil buffer 이용한 것과 그렇지 않은 것에 대한 그림 보기(m)

Figure. stencil buffer 이용하지 않고 그림

Figure. stencil buffer 이용하여 그림

stencil buffer 공부하기 위한 가장 좋은 방법은 이전의 활용 예를 확인하는 것이며, 이를 통해 필요에 따른 다양한 응용 능력을 기를 있다.

Table of Contents

· 8.1 스텐실 버퍼 이용하기

o 8.1.1 스텐실 버퍼 요청하기

o 8.1.2 스텐실 테스트

o 8.1.3 스텐실 테스트 제어하기

§ 8.1.3.1 스텐실 참조

§ 8.1.3.2 스텐실 마스크

§ 8.1.3.3 스텐실

§ 8.1.3.4 비교 연산자

o 8.1.4 스텐실 버퍼 갱신하기

o 8.1.5 스텐실 쓰기 마스크

· 8.2 예제 애플리케이션: 거울

o 8.2.1 반사를 위한 수학

o 8.2.2 거울 구현의 개관

o 8.2.3 코드와 설명

§ 8.2.3.1 스텐실 버퍼 활성화와 관련 렌더 상태 지정

§ 8.2.3.2 스텐실 버퍼에 거울 렌더링

§ 8.2.3.3 거울로 렌더링 부분 표시

§ 8.2.3.4 장면 내에서 반사될 위치 지정

§ 8.2.3.5 반사된 주전자 그리기

· 8.3 예제 애플리케이션: 평면 그림자

o 8.3.1 평행 그림자

o 8.3.2 조명 그림자

o 8.3.3 그림자

o 8.3.4 더블 블렌딩을 막기 위한 스텐실 버퍼 이용

o 8.3.5 코드와 설명

8.1 스텐실 버퍼 이용하기

stencil buffer 이용하기 위해서는 먼저 Direct3D 초기화하는 시점에 stencil buffer 요청해야 하며, 이용할 이를 활성화 시켜야 한다. stencil buffer 활성화/비활성화 하려면 D3DRS_STENCILENABLE 렌더 상태를 IDirect3DDevice9::SetRenderState 통해 true/false 지정해야 한다. stencil buffer 디폴트 값으로 되돌리기 위해서는 IDirect3DDevice9::Clear 이용한다:

// 세번째 인자로 D3DCLEAR_STENCIL 넣고, 여섯번째 인자는 stencil buffer clear하는데 이용될 값을 지정하는 것으로 여기에서는 0 이용하였다.
Device->Clear( 0, 0, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER | D3DCLEAR_STENCIL, 0xff000000, 1.0f, 0 );
...
Device->SetRenderState( D3DRS_STENCILENABLE, true );
...
// stencil 관련 작업을 수행한다.
...
Device->SetRenderState( D3DRS_STENCILENABLE, false );

책에서는 다루고 있지 않지만 DirectX 9.0 그림자 볼륨을 그리는 필요한 렌더링 단계를 축소하여 그림자 볼륨의 속도를 향상시켜주는 "Two-Sided Stencil" 기능을 포함하고 있다. 자세한 내용은 여기에서 관련된 토픽을 찾아보자.

"Creating Reflections and Shadows Using Stencil Buffers (Mark J. Kilgard)" 따르면 depth buffer 이용하는 요즘의 하드웨어에서는 stencil buffer 이용하는데 드는 비용이 거의 존재하지 않는다고 한다.

8.1.1 스텐실 버퍼 요청하기

stencil buffer depth buffer 만들 함께 만들 있으며, depth buffer 포맷을 지정할 stencil buffer 포맷도 함께 지정할 있다. 실제로 stencil buffer depth buffer 동일한 off-screen 버퍼를 공유하며, 픽셀 내의 메모리 세그먼트만 해당 buffer 이용될 뿐이다.

D3DFMT_D24S8

32비트 depth/stencil buffer 만들어 depth buffer에는 픽셀 24비트를, stencil buffer에는 픽셀 8비트를 할당한다.

D3DFMT_D24X4S4

32비트 depth/stencil buffer 만들어 depth buffer에는 픽셀 24비트를, stencil buffer에는 픽셀 4비트를 할당한다(나머지 4비트는 사용하지 않는다).

D3DFMT_D15S1

32비트 depth/stencil buffer 만들어 depth buffer에는 픽셀 15비트를, stencil buffer에는 픽셀 1비트를 할당한다.

일부 그래픽 카드는 8비트 stencil buffer 지원하지 않는다.

8.1.2 스텐실 테스트

앞서 언급했듯이 back buffer 일부 영역이 렌더링되는 것을 막는 stencil buffer 이용할 있는데, 여기서 특정 피셀의 렌더링을 막을 것인지의 결정은 stencil test 통해 이루어지며, 이는 다음과 같은 표현식으로 나타낼 있다.

(참조 & 마스크) 비교 연산자 ( & 마스크)

stencil 활성화되어 있다는 가정하에 모든 픽셀에 대해 stencil test 수행되며, 개의 피연산자를 이용한다.

  • 왼쪽 피연산자(LHS, 참조 & 마스크): 애플리케이션에서 정의한 stencil 참조 값과 마스크 값의 AND 연산으로 얻어진다.
  • 오른쪽 피연산자(RHS, & 마스크): 현재 테스트하려는 픽셀의 stencil buffer 애플리케이션에서 정의한 마스크 값의 AND 연산으로 얻어진다.

비교 연산자에 지정된 방법으로 LHS RHS 비교하는 stencil test 수행한 후에 값이 true이면 back buffer 픽셀을 출력하며, false이면 픽셀이 출력되지 않는다. 당연하지만, back buffer 픽셀이 쓰여지지 않으면 depth buffer에도 쓰여지지 않는다.

8.1.3 스텐실 테스트 제어하기

Direct3D stencil test 이용되는 변수들을 제어(참조/매스크 값과 비교 연산자) 있는 방법을 제공한다.

8.1.3.1 스텐실 참조

stecil 참조 값은 디폴트로 0이지만 D3DRS_STENCILREF 렌더 상태를 이용해 값을 바꿀 있다.

Device->SetRenderState( D3DRS_STENCILREF, 0x1 );
// 16진수를 이용하면 정수의 비트 배열을 확인하거나 AND 등의 비트 연산 시에 유리하다.

8.1.3.2 스텐실 마스크

stencil 마스크 값은 참조와 변수 양쪽의 비트를 마스크하는 데에 이용된다. 디폴트 마스크는 0xffffffff이며, 이는 어떤 비트도 마스크하지 않겠다는 의미이다. D3DRS_STENCILMASK 렌더 상태를 이용하여 변경할 있다.

// 상위 16비트를 마스크한다.
Device->SetRenderState( D3DRS_STENCILMASK, 0x0000ffff );

8.1.3.3 스텐실

값은 stencil test 수행하고 있는 현재 픽셀의 stencil buffer 값이다. 각각의 stencil 값을 지정할 수는 없지만 stencil buffer clear 수는 있다. 또한 부가적으로 stencil 렌더 상태를 이용하면 stencil buffer 쓰여질 것에 대한 제어가 가능하다.

8.1.3.4 비교 연산자

D3DRS_STENCILFUNC 렌더 상태를 이용하여 비교 연산자를 지정할 있으며 값으로는 D3DCMPFUNC 열거형의 멤버 하나이다:

typedef enum _D3DCMPFUNC {
D3DCMP_NEVER = 1, // stencil test 항상 실패한다.
D3DCMP_LESS = 2, // LHS < RHS 경우 stencil test 성공한다.
D3DCMP_EQUAL = 3, // LHS = RHS 경우 stencil test 성공한다.
D3DCMP_LESSEQUAL = 4, // LHS <= RHS 경우 stencil test 성공한다.
D3DCMP_GREATER = 5, // LHS > RHS 경우 stencil test 성공한다.
D3DCMP_NOTEQUAL = 6, // LHS != RHS 경우 stencil test 성공한다.
D3DCMP_GREATEREQUAL = 7, // LHS >= RHS 경우 stencil test 성공한다.
D3DCMP_ALWAYS = 8, // stencil test 항상 성공한다.
D3DCMP_FORCE_DWORD = 0x7fffffff
} D3DCMPFUNC;

8.1.4 스텐실 버퍼 갱신하기

특정 픽셀이 back buffer 쓰여질지 여부를 결정하는 이외에도 다음과 같은 가지 경우에는 stencil buffer 항목이 갱신되는 방법을 정의할 있다:

(i, j)번째 픽셀에서 stencil test 실패

D3DRS_STENCILFAIL 렌더 상태를 지정하여 이러한 상황이 발생했을 stencil buffer 내의 (i, j)번째 항목을 갱신하는 방법을 정의할 있다.

Device->SetRenderState( D3DRS_STENCILFAIL, StencilOperation );

(i, j)번째 픽셀에서 depth test 실패

D3DRS_STENCILZFAIL 렌더 상태를 지정하여 (i, j)번째 항목이 갱신하는 방법을 정의할 있다.

Device->SetRenderState( D3DRS_STENCILZFAIL, StencilOperation );

(i, j)번째 픽셀에서 depth test stencil test 성공

D3DRS_STENCILPASS 렌더 상태를 지정하여 (i, j)번째 항목을 갱신하는 방법을 정의할 있다.

Device->SetRenderState( D3DRS_STENCILPASS, StencilOperation );

여기에서 사용되는 StencilOperation 값에는 다음 상수 하나를 사용할 있다:

D3DSTENCILOP_KEEP

stencil buffer 항목을 변경하지 않는다(현재의 값을 유지한다).

D3DSTENCILOP_ZERO

stencil buffer 항목을 0으로 지정한다.

D3DSTENCILOP_REPLACE

stencil buffer 항목을 stencil 참조 값으로 대체한다.

D3DSTENCILOP_INCRSAT

stencil buffer 항목을 증가시킨다(증가된 값은 최대치를 넘지 않는다).

D3DSTENCILOP_DECRSAT

stencil buffer 항목을 감소시킨다(감소된 값은 0보다 작지 않다).

D3DSTENCILOP_INVERT

stencil buffer 항목을 반전시킨다.

D3DSTENCILOP_INCR

stencil buffer 항목을 증가시킨다(증가된 값이 최대치를 넘을 경우 0으로 돌려진다).

D3DSTENCILOP_DECR

stencil buffer 항목을 감소시킨다(감소된 값이 0보다 작을 경우 최대치로 돌려진다).

8.1.5 스텐실 쓰기 마스크

지금까지 언급한 렌더 상태 이외에도 stencil buffer 쓰여지는 모든 값을 마스크하는 쓰기 마스크를 설정할 수도 있다. 쓰기 마스크의 디폴트 값은 0xffffffff이며, D3DRS_STENCILWRITEMASK 렌더 상태를 이용하여 지정할 있다:

// 상위 16비트를 마스크한다.
Device->SetRenderState( D3DRS_STENCILWRITEMASK, 0x0000ffff );

8.2 예제 애플리케이션: 거울

예제 파일 다운로드: chapter8_source.zip (8 예제 모두 포함)

Figure. 거울 예제 실행 화면

단순한 구현을 위해서 평평한 표면의 거울을 구현하는 것으로 내용을 제한한다. 거울을 구현하기 위해서는 다음 가지 문제를 해결해야 한다:

올바르게 반사를 그려내기 위해 임의의 평면에 물체가 반사되는 방법을 알아야 한다.

벡터 기하학을 통해 해결할 있다.

거울 영역에만 반사 효과가 나타나도록 해야 한다.

stencil buffer 통해 해결할 있다.

8.2.1 반사를 위한 수학

임의의 평면 n^ . p + d = 0 v = (vx, vy, vz) 반사된 v' = (v'x, v'y, v'z) 계산하는 방법을 알아본다. 다음 그림을 참고하자:

Figure. 임의의 평면에 대한 반사. k v에서 평면으로 부호를 가진 최단거리이며, 그림에서 v 평면의 양의 영역에 있으므로 k역시 양수가 된다.

PART 1 "평면" 섹션에서 q = v - kn^임을 배웠다. k v에서 평면으로의 부호를 가진 최단거리이므로, 평면 (n^, d) 대한 v 반사는 다음과 같이 얻을 있다:

v' = v - 2kn^
= v - 2(n^ . v + d)n^
= v - 2( (n^ . v)n^ + dn^ )

v에서 v'로의 변환은 다음 행렬로 표현할 있다.

D3DX 라이브러리는 R 같은 임의의 평면에 대한 반사 행렬을 만들어내는 함수, D3DXMatrixReflect 제공한다:

D3DXMATRIX *WINAPI D3DXMatrixReflect(
D3DXMATRIX *pOut, // 결과 행렬
CONST D3DXPLANE *pPlane // 반사할 평면
);

여기에서는 반사 변환에 대해 살펴보고 있으므로 반사 변환의 가지 특수한 경우를 확인해보도록 하자. 가지 특수한 경우란 표준 좌표 평면인 yz 평면, xz 평면, xy 평면을 말하는 것으로 다음과 같은 가지의 행렬을 통해 나타낼 있는데, 만약, yz 평면 반대쪽의 포인트를 반사하기 위해서는 x성분의 반대를 취하면 된다. 나머지 경우도 비슷하다.

8.2.2 거울 구현의 개관

거울을 구현할 중요한 요점 가지는 거울의 앞에서만 반사된다는 것이다. 경우 물체가 거울 앞에 있는지를 공간적으로 확인한다면 너무 복잡한 작업이 필요할 것이다. 따라서, 거울을 포함하는 모든 표면에서 항상 물체를 반사시키도록 렌더링하는 방법을 선택하고, stencil buffer 이용하여 back buffer내의 특정 영역이 렌더링되는 것을 막아준다. 이와 같은 작업을 위해서는 다음과 같은 순서를 따른다:

1. 바닥과 , 거울, 주전자를 포함하는 전체 장면을 (보통 때와 마찬가지로) 렌더링한다. 아직 주전자의 반사는 포함되지 않으며, 단계에서는 아직 stencil buffer 수정하지 않는다.

2. stencil buffer 0으로 clear한다.

3. 거울을 구성하는 기본형을 stencil buffer에만 렌더링한 다음, stencil test 항상 성공(D3DCMP_ALWAYS)하도록 렌더 상태를 변경하고 test 성공하면 stencil buffer 항목을 1 대체(D3DSTENCILOP_REPLACE)하도록 지정한다. 이렇게 하면 거울만을 렌더링하는 것이므로 거울에 해당하는 픽셀만 1이란 값을 가지며, 나머지 영역은 0 된다.

4. 이제 반사된 주전자를 back buffer stencil buffer 렌더링한다. 하지만 이번에는 stencil test 통과한 부분만 back buffer 렌더링 된다. , stencil buffer 항목이 1 경우에만 테스트를 통과하도록 지정한다. stencil buffer 내의 거울에 해당하는 항목만이 1 값을 가지므로 반사된 주전자는 거울에만 렌더링된다.

8.2.3 코드와 설명

예제와 관련된 코드는 RenderMirror 함수에 포함되어 있다. 함수는 stencil buffer 거울을 렌더링하고, 거울에 해당되는 부분에만 반사된 주전자를 렌더링 한다. 코드를 단계적으로 살펴보자.

8.2.3.1 스텐실 버퍼 활성화와 관련 렌더 상태 지정

Device->SetRenderState(D3DRS_STENCILENABLE, true); // 스텐실 활성화
Device->SetRenderState(D3DRS_STENCILFUNC, D3DCMP_ALWAYS); // 스텐실 테스트가 항상 성공하도록 지정
Device->SetRenderState(D3DRS_STENCILREF, 0x1); //
Device->SetRenderState(D3DRS_STENCILMASK, 0xffffffff); //
Device->SetRenderState(D3DRS_STENCILWRITEMASK, 0xffffffff); //
Device->SetRenderState(D3DRS_STENCILZFAIL, D3DSTENCILOP_KEEP); // 깊이 테스트가 실패하면 스텐실 버퍼 항목을 유지함
Device->SetRenderState(D3DRS_STENCILFAIL, D3DSTENCILOP_KEEP); // 스텐실 테스트가 실패하면 스텐실 버퍼 항목을 유지함(D3DCMP_ALWAYS 인해 테스트가 실패하지는 않는다)
Device->SetRenderState(D3DRS_STENCILPASS, D3DSTENCILOP_REPLACE); // 깊이/스텐실 테스트가 모두 성공하면 스텐실 버퍼 항목을 스텐실 참조 , 0x1 대체

8.2.3.2 스텐실 버퍼에 거울 렌더링

D3DRS_ZWRITEENABLE false 지정하면 depth buffer 쓰여지는 것을 막을 있다. , D3DRS_SRCBLEND D3DBLEND_ZERO, D3DRS_DESTBLEND D3DBLEND_ONE으로 지정하고 블렌딩을 이용하면 back buffer 갱신되는 것을 막을 있다. 이는 blending 방정식에 직접 인수를 넣어 확인할 있다:

Pixelresult
= Pixelsource
(0, 0, 0, 0) + Pixeldestination (1, 1, 1, 1)
= (0, 0, 0, 0) + Pixeldestination
= Pixeldestination

// disable writes to the depth and back buffers
Device->SetRenderState(D3DRS_ZWRITEENABLE, false);
Device->SetRenderState(D3DRS_ALPHABLENDENABLE, true);
Device->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_ZERO);
Device->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_ONE);

// draw the mirror to the stencil buffer
Device->SetStreamSource(0, VB, 0, sizeof(Vertex));
Device->SetFVF(Vertex::FVF);
Device->SetMaterial(&MirrorMtrl);
Device->SetTexture(0, MirrorTex);
D3DXMATRIX I;
D3DXMatrixIdentity(&I);
Device->SetTransform(D3DTS_WORLD, &I);
Device->DrawPrimitive(D3DPT_TRIANGLELIST, 18, 2);

// re-enable depth writes
Device->SetRenderState( D3DRS_ZWRITEENABLE, true );

8.2.3.3 거울로 렌더링 부분 표시

이제 stencil buffer내의 거울에 해당하는 픽셀은 0x1 값을 가지게 된다. 이제 주전자 렌더링을 준비한다.

// only draw reflected teapot to the pixels where the mirror was drawn to.
Device->SetRenderState(D3DRS_STENCILFUNC, D3DCMP_EQUAL);
Device->SetRenderState(D3DRS_STENCILPASS, D3DSTENCILOP_KEEP);

새로운 비교 연산자를 지정하고 다음과 같이 stencil test 구성한다:

(ref & mask) == (value & mask)
(0x1 & 0xffffffff) == (value & 0xffffffff)
(0x1) == (value & 0xffffffff)

이제, 반사된 주전자 중에서 거울에 비취는 부분만 렌더링된다.

8.2.3.4 장면 내에서 반사될 위치 지정

반사될 위치를 지정하기 위해 먼저 반사되지 않은 주전자 위치로 이동한 , xy 평면으로 반사를 수행한다(변환의 순서는 행렬을 곱하는 순서에 따라 정해진다).

// position reflection
D3DXMATRIX W, T, R;
D3DXPLANE plane(0.0f, 0.0f, 1.0f, 0.0f);
D3DXMatrixReflect(&R, &plane);

D3DXMatrixTranslation(&T,
TeapotPosition.x,
TeapotPosition.y,
TeapotPosition.z);

W = T * R;

8.2.3.5 반사된 주전자 그리기

// 반사된 주전자의 깊이가 거울의 깊이보다 크므로(거울이 반사된 주전자를 가리므로),
// 현재 상태에서는 반사된 주전자를 그려도 나타나지 않는다. 따라서, depth buffer clear한다.
Device->Clear(0, 0, D3DCLEAR_ZBUFFER, 0, 1.0f, 0);
// 단순히 depth buffer clear하면 반사된 주전자가 거울 전면에 그려지게 되는데,
// 이는 원하는 결과와는 차이가 있다. 따라서,
// depth buffer clear함과 동시에 반사된 주전자를 거울과 섞어(blend) 주어야 한다.

// 거울과 반사된 주전자와의 blending한다:
// result_pixel = source_pixel dest_pixel + dest_pixel (0, 0, 0, 0)
// = source_pixel dest_pixel
Device->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_DESTCOLOR);
Device->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_ZERO);

// 반사된 주전자를 그릴 준비가 완료 되었다. 이제 반사된 주전자를 그린다.

Device->SetTransform(D3DTS_WORLD, &W); // W 반사된 주전자를 장면 내의 적절한 위치로 이동시킨다.
Device->SetMaterial(&TeapotMtrl);
Device->SetTexture(0, 0);
Device->SetRenderState(D3DRS_CULLMODE, D3DCULL_CW); // 물체가 반사될 때에 물체의 전면과 후면이 뒤바뀌기는데 반해 winding-order 유지된다.
// 따라서, winding-order 변경시키기 위해 후면 추려내기 방법을 변경해야 한다.
Teapot->DrawSubset(0);

// 작업이 끝나면 렌더 상태를 되돌려 준다.
Device->SetRenderState(D3DRS_ALPHABLENDENABLE, false); // blending 비활성화
Device->SetRenderState( D3DRS_STENCILENABLE, false); // stencil 비활성화
Device->SetRenderState(D3DRS_CULLMODE, D3DCULL_CCW); // winding-order 원래대로

8.3 예제 애플리케이션: 평면 그림자

예제 파일 다운로드: chapter8_source.zip (8 예제 모두 포함)

Figure. 그림자 예제 실행 화면

이와 같은 형태의 그림자는 매우 간소화된 것으로 장면의 사실감을 높여주기는 하지만 그림자 볼륨과 같이 사실적이지는 못하다. 자세한 내용은 여기에서 관련된 토픽을 찾아보도록 하자.

평면 그림자를 구현하기 위해서는 먼저 물체가 만들어내는 그림자를 찾고 렌더링할 있도록 기하학적으로 구성해야 다음, 약간의 투명도(예제에서는 50%) 가진 검은 재질을 이용해 렌더링 하면 된다. 이와 같은 렌더링에는 "double blending"이라 불리는 부작용이 따르는데, stencil buffer 이용해 이를 해결할 있다. 이에 대해서는 뒤에서 살펴본다.

8.3.1 평행 그림자

Figure. 평행 광원에 의해 발생하는 그림자

그림에서 L 방향을 가지는 평행 광원에서 p 통하는 광선은 r(t) = p + tL 얻을 있고, 광선 r(t) 평면 n . p + d = 0 교차하면 s 얻을 있다. 또한 광선 r(t) 물체의 점에서 평면으로 발사해 얻은 교차점의 집합으로 그림자의 기하 정보를 정의할 있다. 교차점 s 광선/평면 교차 테스트를 통해 간단히 얻을 있다:

1. n . (p + tL) + d = 0 : r(t) 평면 방정식에 넣는다.

2. n . p + t(n . L) = -d

3. t(n . L) = -d - n . p : t 풀어낸다.

4. t = (-d - n . p) / (n . L)

s = p + ((-d - n . p) / (n . L))L

8.3.2 조명 그림자

Figure. 조명 광원에 의해 발생하는 그림자

그림에서 L 위치에 존재하는 조명 광원에 의해 발생하는 그림자를 보여주고 있다. 조명에서 p 발사되는 광선은 r(t) = p + t(p - L) 얻을 있으며, 광선 r(t) 평면 n . p + d = 0과의 교차점으로 s 얻을 있다. 광선 r(t) 물체의 점에서 평면으로 발사해 얻은 교차점의 집합으로 그림자의 기하 정보를 얻을 있으며, 평행 그림자에서와 같은 방법(평면/광선 교차)으로 s 풀어낼 있다.

8.3.3 그림자 행렬

평행 조명을 보여주는 그림에서 그림자는 결국 지정된 광선 방향으로 평면 n . p + d = 0 물체를 평행 투영한 것이다. 비슷하게 조명을 보여주는 그림자는 광원의 관점에서 물체를 평면 n . p + d = 0 원근 투영한 것이다.

p에서 평면 n . p + d = 0으로의 투영 s 하나의 행렬로 표현될 있다. 또한, 동일한 행렬로 직각 투영과 원근 투영을 동시에 표현하는 것도 가능하다.

그림자가 만들어질 평면의 공통 평면 방정식은, 계수로 4D 벡터 (nx, ny, nz, d) 이용하고, 평행 조명의 방향이나 조명의 위치를 표현하는 4D 벡터로 L = (Lx, Ly, Lz, Lw) 이용하자. w 다음과 같이 이용된다:

1. w = 0이면, L 평행 조명의 방향을 나타낸다.

2. w = 1이면, L 조명의 위치를 나타낸다.

평면의 법선이 정규화되었다고 가정하고 k = (nx, ny, nz, d) . (Lx, Ly, Lz, Lw) = nxLx + nyLy + nzLz + dLw라고 하자.

이제 다음의 그림자 행렬을 이용해 p에서 투영 s로의 변환을 나타낼 있다.

행렬의 유도 과정은 우리에게 그다지 중요한 내용이 아니므로 다루지 않는다. 관심 있는 사람은 Jim Blinn's Corner: A Trip Down the Graphics Pipeline에서 6 "Me and My (Fake) Shadow" 부분을 참고하기 바란다.

D3DX 라이브러리에서는 그림자 행렬을 만들어내는 D3DXMatrixShadow 제공한다. 결과로 얻어진 행렬은 w = 0 경우 평행 조명에서, w=1 경우 조명에서 주어진 평면으로 그림자를 투영한다:

D3DXMATRIX *WINAPI D3DXMatrixShadow(
D3DXMATRIX *pOut, // 결과 행렬
CONST D3DXVECTOR4 *pLight, // L
CONST D3DXPLANE *pPlane // 그림자를 만들 평면
);

8.3.4 더블 블렌딩을 막기 위한 스텐실 버퍼 이용

물체의 기하 정보를 평면에 납작하게 만들어 그림자를 표현하면 이상의 펴진 삼각형이 겹치는 현상이 발생할 있다. (blending 이용해) 반투명한 그림자를 렌더링하면 겹쳐진 영역들이 여러 차례 블렌드 되어 더욱 어둡게 나타난다. 이를 해결하기 위한 방법이 바로 stencil buffer이다. , 처음으로 렌더링되는 픽셀만을 받아들이도록 stencil test 구성하여 하나의 픽셀에 이상 blending 적용되는 것을 막을 있다.

8.3.5 코드와 설명

예제와 관련된 코드는 RenderShadow 함수에 포함되어 있다. 여기에서는 stencil buffer 이미 0으로 clear했다고 가정하고 있다는 점에 주의하자.

void RenderShadow()

{

// stencil buffer 이미 0으로 clear했다고 가정한다.

/*

* 먼저 stencil 렌더 상태를 지정하고, 비교 함수를 D3DCMP_EQUAL 지정한 다음,

* D3DRS_STENCILREF 0x0으로 지정하여 stencil buffer 내의 대응되는 항목이 0x0 경우에만

* back buffer 그림자를 렌더링하도록 하였다.

* stencil buffer 0(0x0)으로 clear되어 있으므로 처음 픽셀을 때에는 항상 테스트가 성공하지만

* D3DRS_STENCILPASS D3DSTENCILOP_INCR 지정하였으므로

* 이미 쓰여진 픽셀을 쓰려고 때는 테스트가 실패한다.

* , 픽셀의 덮어쓰기를 막는 방법으로 double-blending 현상을 제거한 것이다

*/

Device->SetRenderState(D3DRS_STENCILENABLE, true);

Device->SetRenderState(D3DRS_STENCILFUNC, D3DCMP_EQUAL);

Device->SetRenderState(D3DRS_STENCILREF, 0x0);

Device->SetRenderState(D3DRS_STENCILMASK, 0xffffffff);

Device->SetRenderState(D3DRS_STENCILWRITEMASK, 0xffffffff);

Device->SetRenderState(D3DRS_STENCILZFAIL, D3DSTENCILOP_KEEP);

Device->SetRenderState(D3DRS_STENCILFAIL, D3DSTENCILOP_KEEP);

Device->SetRenderState(D3DRS_STENCILPASS, D3DSTENCILOP_INCR); \// increment to 1

/*

* 다음에는 그림자 변환을 계산하고 장면 내의 적절한 위치로 그림자를 이동한다:

*/

// position shadow

D3DXVECTOR4 lightDirection(0.707f, -0.707f, 0.707f, 0.0f);

D3DXPLANE groundPlane(0.0f, -1.0f, 0.0f, 0.0f);

D3DXMATRIX S;

D3DXMatrixShadow(

&S,

&lightDirection,

&groundPlane);

D3DXMATRIX T;

D3DXMatrixTranslation(

&T,

TeapotPosition.x,

TeapotPosition.y,

TeapotPosition.z);

D3DXMATRIX W = T * S;

Device->SetTransform(D3DTS_WORLD, &W);

/*

* 마지막으로 50% 투명도를 갖는 검은 재질을 지정하고 depth test 비활성화 다음

* 그림자를 렌더링 한다. 렌더링 후에는 다시 렌더 상태를 원래대로 되돌려 준다.

* z-쟁탈을 막기 위해 depth buffer 비활성화 하였는데, 바닥을 먼저 렌더링하고

* depth test 상태로 그림자를 렌더링하면 의도한대로 바닥 위에 그림자를 그릴 있다.

*/

// alpha blend the shadow

Device->SetRenderState(D3DRS_ALPHABLENDENABLE, true);

Device->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);

Device->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);

D3DMATERIAL9 mtrl = d3d::InitMtrl(d3d::BLACK, d3d::BLACK, d3d::BLACK, d3d::BLACK, 0.0f);

mtrl.Diffuse.a = 0.5f; // 50% transparency.

// Disable depth buffer so that z-fighting doesn't occur when we

// render the shadow on top of the floor.

Device->SetRenderState(D3DRS_ZENABLE, false);

Device->SetMaterial(&mtrl);

Device->SetTexture(0, 0);

Teapot->DrawSubset(0);

Device->SetRenderState(D3DRS_ZENABLE, true);

Device->SetRenderState(D3DRS_ALPHABLENDENABLE, false);

Device->SetRenderState(D3DRS_STENCILENABLE, false);

역주 : 이 글에 대한 번역본은 없는 것 같아서 올려 봅니다. 예전에도 번역했던 것 같은 기억이 있는데... 홈페이지를 날려 먹어서 기억이 안 나네요. 일단 소개의 링크에 있는 글은 한 번씩 읽어보시기 바랍니다.

 

원문 : http://www.gamedev.net/reference/articles/article2238.asp

 

Texture Splatting In Direct3D

 

by Nate Glasser

 

 

소개

 

만약 지형 텍스처링 기법에 대해 공부해 왔다면, 아마도 텍스처 스플래팅에 대해서 들어 보았을 것이다. 이 개념은 Charles Bloom 에 의해서 만들어졌는데, 그는http://www.cbloom.com/3d/techdocs/splatting.txt 에서 이에 대해 논의하고 있다 (번역본은http://blog.naver.com/dkdn111/8941198 에 있음. 번역자에 대해서는 그 글의 상위에 있음). Charles Bloom 을 무시하려는 것은 아니지만, 그것은 더 이상 명확하거나 간결한 기사가 아니며, 혼란스러움을 남겨 왔다. 많이 사용되고 추천되는 반면, 일부는 그것을 충분히 설명하는데 시간을 할애해 왔다. 나는 그것을 둘러 싼 의혹들을 명확히 하고 당신의 지형 엔진에서 그것을 구현하는 방법에 대해서 설명하고자 한다.

 

기초

 

텍스처 스플래팅이란 무엇인가? 가장 간단한 형식으로 살펴 보자면 그것은 알파맵을 사용하여 서피스 상의 텍스처를 서로 블렌딩하는 방식이다.

 

나는 알파맵이라는 개념을 사용하여 단일 채널의 텍스처에 존재하는 그레이스케일 이미지를 참조할 것이다. 그것은 어떠한 채널이라도 될 수 있다. alpha, red, green, blue, luminance. 텍스처 스플래팅에서, 그것은 주어진 위치에서 텍스처가 얼마나 가시화될 것인지를 제어하는데 사용될 것이다. 그것은 단순한 곱셈( 알파맵 * 텍스처 ) 에 의해서 수행될 것이다. 만약 알파맵의 텍셀 값이 1 이라면 그 텍스처는 그곳에서 전체 값으로 보일 것이다; 만약 알파맵의 텍셀 값이 0 이라면, 그 텍스처는 그곳에서 전혀 보이지 않게 될 것이다.

 

지형에 대해 그 텍스처는 잔디, 진흙, 암석, 눈, 또는 당신이 생각할 수 있는 여러 가지 유형의  지형일 수 있다. Bloom 은 텍스처 및 그것의 관련 알파맵을 스플랫(splat) 으로서 참조한다. 그것은 캔버스 위에 떨어진 페인트 방울(glob)로 비유될 수 있다. 스플랫은 당신이 그 페인트 방울을 볼 수 있는 모든 곳에 존재한다. 다중의 페인트 방울은 서로의 위에 겹쳐서 최종 그림을 만들어 낸다.

 

당신이 128 x 128 높이맵 지형을 가지고 잇으며, 32 x 32 의 크기를 가진 청크로 나누었다고 하자. 각 청크는 33 x 33 개의 정점으로 구성된다. 각 청크는 그 위에서 여러 번 반복되는 기저 텍스처를 가지고 있지만, 알파맵은 전체 영역에 대해서 펼쳐진다. 청크의 (0, 0) 좌표는 알파맵 좌표 (0, 0)을 가지며, 텍스처 좌표 (0, 0)을 가진다. 청크의 (33, 33) 좌표는 알파맵 좌표 (1, 1) 을 가지며, 텍스처 좌표 (x, x)를 가진다. 여기에서 x 는 텍스처가 반복되는 회수를 의미한다. x 는 텍스처 해상도에 달려 있다. 가까운 것은 충분히 반복할 필요가 있지만, 먼 것은 그렇게 많이 반복할 필요가 없다.

 

청크당 알파맵의 해상도는 당신 맘대로 하면 되지만, 2의 배승수를 추천한다. 32 x 32 청크에 대해 당신은 32 x 32 알파맵(유닛당 1텍셀), 64 x 64 알파맵(유닛당 2텍셀), 128 x 128 알파맵(유닛당 4 텍셀)을 가질 수 있다.(역주 : 여기에서 유닛(unit)이라고 하는 것은 타일을 의미한다.) 해상도를 결정할 때 주어진 청크 상에서 가시화되는 모든 텍스처에 대해서 알파맵을 필요로 한다는 것을 명심하기 바란다. 해상도가 높을 수록 블렌딩에 대한 제어는 더 많이 필요하고, 메모리를 더 필요로 하게 된다.

 

청크의 크기를 결정하는 것은 약간 미묘하다. 너무 작으면 상태 변경과 드로우 호출을 너무 많이 하게 되고, 너무 크면 알파맵의 대부분의 영역이 빈 공간이 된다. 예를 들어 유닛당 1텍셀을 가진 알파맵을 128 x 128 청크와 함께 생성하기로 했다고 할 때, 알파맵의 0 이 아닌 값이 4 x 4 영역이라면, 알파맵의 124 x 124 만큼의 메모리가 낭비된 것이다. 만약 청크 크기가 32 x 32 라면 단지 28 x 28 의 메모리만이 낭비될 것이다. 이것은 중요한 점을 상기시킨다 : 만약 주어진 텍스처가 주어진 청크에서 전혀 나타나지 않는다면, 그 청크에 그 텍스처를 위한 알파맵을 만들지 말라.

 

지형이 청크로 나뉘는 이유가 이제 나온다. 첫째 가장 중요한 부분으로 그것은 비디오 메모리를 절약할 수 있다. 둘째 그것은 필레이트(fillrate) 소비를 줄여줄 수 있다. 작은 텍스처를 사용함으로써 텍스처가 모든 청크에서 나타나지 않을 경우 비디오 카드가 수행해야 할 샘플링이 줄어든다. 셋째 그것은 지형이 어떤 식으로든 청크로 나뉘는 것을 요구하는 geomipmapping 과 같은 일반적인 LOD 기법에 들어 맞는다.

 

블렌드 생성하기

 

부드러운 블렌딩을 획득하는 핵심은 알파맵의 선형 보간이다. 0 다음에 바로 1이 온다고 가정하자. 알파맵이 전체 지형에 펼쳐질 때, Direct3D 는 두 값 사이의 블렌드를 생성한다. 그리고 나서 펼쳐진 알파맵은 지형 텍스처와 결합하여 텍스처 자체가 블렌딩되게 만든다. 

 

Rendering then becomes the simple matter of going through each chunk and rendering the splats on it. 일반적으로 첫 번째 스플랫은 완전히 불투명할 것이며, 그 다음의 스플랫들은 알파맵에서 값이 변할 것이다. 특정한 상황에 대해 설명하도록 하겠다. 첫 번째 스플랫이 진흙이라고 하자. 그것은 먼저 청크상에 나타나기 때문에, 완전히 채워진(solid) 알파맵을 가지게 될 것이다. 

첫 번째 스플랫이 렌더링된 후, 이 청크는 진흙으로 뒤덮힌다. 그리고 나서 그 위에 잔디 레이어가 추가된다 :

이 작업은 청크의 나머지 스플랫에 대해서 반복된다.

 

중요한 것은 각 청크에 대해서 같은 순서로 모든 것을 렌더링한다는 것이다. 스플랫 덧셈에는 교환법칙이 성립하지 않는다. 스플랫을 건너 뛰는 것은 어떠한 해도 끼치지 않지만, 순서를 변경하는 것은 다음과 같이 다른 모양으로 나타나게 만든다 :

잔디 스플랫은 가려진다. 왜냐하면 진흙 스플랫이 완전히 불투명하고 두 번째로 렌더링되었기 때문이다.

 

당신은 왜 첫 번째 스플랫이 불투명해야만 하는지에 대해 궁금해 할 것이다. 그것이 불투명하지 않다고 하자. 그리고 대신에 잔디 스플랫이 존재하는 곳만 채워져 있다고 하자. 다음과 같은 일이 발생할 것이다 :  

이전에 블렌딩 했던 것과 비교했을 때 좋게 보이지 않는다는 것은 명확하다. 첫 번째 스플랫을 완전히 불투명하게 만듦으로써 당신은 위의 그림처럼 나타나는 것을 막을 수 있다.
 
알파맵 생성하기
 
이제 우리는 텍스처 스플래팅이 무엇인지 알게 되었다. 우리는 캔버스를 기술하기 위해서 알파맵을 생성할 필요가 있다. 그러나 알파맵에 어떤 값을 부여할 지를 어떻게 결정해야 하는가?

 

어떤 사람들은 지형 높이에 기반해서 그것을 결정하지만, 나는 알파맵을 당신이 원하는데로 만드는 기능을 부여하는 것을 추천한다. 이것은 제약 없이 원하는 곳에 텍스처를 배치하기 위한 유연성을 제공한다. 페인트 프로그램에서 채널을 그리기만 하면 된다. 더 좋은 방법은 아티스트가 알파맵을 확인하고 실제 월드에서 수정해 볼 수 있는 간단한 월드 에디터를 생성하는 것이다. (역주 : 차라리 자동으로 계산하는 것이 훨씬 나을 듯 합니다. 직접 알파맵 제작할 바에야 그냥 통맵 만들고 말지... 그렇지만 에디터에서 알파맵을 수정하는 기능을 추가해 준다면 더 낫겠죠.)

 

구현

 

단계를 다시 돌아보고, 우리가 가진 것들을 살펴 보자 :

  1. 높이맵과 같은 지형 표현의 정렬
  2. 지형에 렌더링될 텍스처 집합
  3. 각 텍스처를 위한 알파맵 

    세 번째를 살펴 보자. 우리는 각 알파맵이 텍스처로 존재해야 함을 알고 있다. 이것은 모든 알파맵이 자신만의 텍스처를 필요로 한다는 것을 의미하는가? 고맙게도 대답은 '그렇지 않다' 이다. 알파맵은 단지 텍스처의 단일 채널로서만 존재하기 때문에, 우리는 네 개의 알파맵을 단일 텍스처로 묶을 수 있다. 하나는 red, 하나는 green, 하나는 blue, 하나는 alpha 에 넣는다. 이들 개별 채널에 접근하기 위해, 우리는 픽셀 쉐이더를 사용할 필요가 있다. 그리고 다섯개의 텍스처를 필요로 하기 때문에(하나는 알파맵과 함께, 넷은 블렌딩을 위해), PS 1.4 가 요구된다. 안타깝게도 이것은 아직까지는 무리한 요구이다. 그래서 나는 픽셀 쉐이더 뿐만 아니라 고정함수 파이프라인을 사용해 텍스처 스플래팅을 사용하는 방법에 대해서도 보여줄 것이다.

     

    고정함수 파이프라인을 사용한 스플래팅

     

    고정 함수 파이프라인을 사용하는 것은 픽셀 쉐이더 기법을 사용하지 않는다는 이점을 가진다 : 그것은 비디오 카드에서 가상적으로 실행될 것이다. 그것이 요구하는 것은 알파맵당 하나의 텍스처 유닛, 텍스처당 하나의 텍스처 유닛, 올바른 블렌딩 상태이다.

     

    나는 알파맵을 스테이지 0에 넣고, 텍스처를 스테이지 1에 넣기로 했다. 이것은 픽셀 쉐이더와의 일관성을 위한 것인데, 픽셀 쉐이더에서는 스테이지 0 에 알파맵을 설정한다. 텍스처 스테이지 스테이트는 그것보다는 상대적으로 직관적이다. 스테이지 0 은 그것은 알파값을 스테이지 1 로 전달한다. 스테이지 1 은 그 알파값을 자신의 것처럼 사용하며 자신의 색상 값과 그것을 한쌍으로 만든다.

     

    // 알파맵 : 알파맵으로부터 알파를 취함. 색상은 신경쓰지 않음.
    g_Direct3DDevice->SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_SELECTARG1);
    g_Direct3DDevice->SetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_TEXTURE);

    // 텍스처 : 텍스처로부터 색상을 취함. 이전 스테이지로부터 알파를 취함.
    g_Direct3DDevice->SetTextureStageState(1, D3DTSS_COLOROP, D3DTOP_SELECTARG1);
    g_Direct3DDevice->SetTextureStageState(1, D3DTSS_COLORARG1, D3DTA_TEXTURE);
    g_Direct3DDevice->SetTextureStageState(1, D3DTSS_ALPHAOP, D3DTOP_SELECTARG1);
    g_Direct3DDevice->SetTextureStageState(1, D3DTSS_ALPHAARG1, D3DTA_CURRENT);

     

    다중 스플랫을 정확하게 결합하기 위해서 블렌딩 렌더링 스테이트도 설정해야만 한다. D3DRS_SRCBLEND 는 렌더링되고 있는 스플랫으로부터 오는 알파이며, 우리는 그것을 D3DBLEND_SRCALPHA 로 설정한다. 우리가 원하는 최종 방정식은 FinalColor = Alpha * Texture + (1 - Alpha) * PreviousColor 이다. 이것은 D3DRS_DESTBLEND 를 D3DBLEN_INVSRCALPHA 로 설정함으로써 수행된다.

     

    g_Direct3DDevice->SetRenderState(D3DRS_ALPHABLENDENABLE, TRUE);
    g_Direct3DDevice->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);
    g_Direct3DDevice->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);

     

    픽셀쉐이더를 사용한 스플래팅

     

    왜 픽셀 쉐이더에 대해서 걱정하는가? 하나의 채널을 사용하는 것보다 텍스처 내의 이용 가능한 모든 채널을 사용하는 것이 메모리를 절약해 준다. 또한 단일 패스에서 네 개의 스플랫을 렌더링할 수 있도록 해 주기 때문에 변환에 필요한 정점 개수를 줄일 수 있다. 결합되는 모든 텍스처는 쉐이더 내에 배치되기 때문에, 고려해야 하는 텍스처 스테이지 스테이트가 존재하지 않는다. 우리는 단지 스테이지 0 에다가 각 채널 단위의 알파맵을 가진 텍스처를 로드하고, 스테이지 1 부터 4까지는 텍스처를 로드하고, 그리고 나서 렌더링하면 된다.

     

    ps_1_4

     

    ////////////////////////////////
    // r0 : 알파맵
    // r1 - r4 : 텍스처
    ////////////////////////////////

     

    // 텍스처 샘플링
    texld r0, t0
    texld r1, t1
    texld r2, t1
    texld r3, t1
    texld r4, t1

     

    // 텍스처들을 그것들의 알파맵에 기반해 결합한다
    mul r1, r1, r0.x
    lrp r2, r0.y, r2, r1
    lrp r3, r0.z, r3, r2
    lrp r0, r0.w, r4, r3

     

    mul 명령어는 첫 번째 텍스처를 그것의 알파맵과 곱하는데, 이는 샘플러 0 의 텍스처의 red 채널에 저장되어 있는 알파값이다. lrp 명령어는 다음의 수학식을 수행한다 : dest = src0 * src1 + (1 - src0) * src2. r0.x 가 r1 에 저장된 진흙 텍스처의 알파맵이라고 하고, r0.y 는 r2 에 저장된 잔디 텍스처의 알파맵이라고 하자. r2 는 첫 번째 lrp 이후에 다음을 포함한다 : GrassAlpha * GrassTexture + (1 - GrassAlpha) * DirtBlended. 여기에서 DirtBlended 는 DirtAlpha * DirtTexture 이다. 당신도 알겠지만 lrp 는 이전에 우리가 설정했던 렌더 스테이트와 텍스처 스테이지 스테이트와 같은 작업을 수행한다. 최종 lrp 는 r0 을 출력 레지스터로 사용하는데, 이는 최종 픽셀 색상으로서 사용되는 레지스터이다. 이것은 최종 mov 명령어에 대한 필요성을 없애 준다.

     

    청크에 대해 두 개 혹은 세 개만의 스플랫을 렌더링할 필요가 있다면 어떻게 하는가? 만약 픽셀 쉐이더를 재사용하고자 한다면, 남은 채널들을 0 으로 채우기만 하면 된다. 그러한 방식을 사용하면 최종 결과에는 영향을 주지 않을 것이다. 아니면 두 개의 스플랫을 렌더링하거나 세 개의 스플랫을 렌더링하는 다른 픽셀 쉐이더를 생성할 수도 있다. 그러나 SetPixelShader 호출에 대한 부가적인 오버헤드는 명령어 두 개를 더 사용하는 것보다 더 안 좋을 수 있다.

    당신이 청크에 대해서 네 개 이상의 스플랫을 렌더링하고자 한다면 다중 패스가 요구된다. 7개의 스플랫을 렌더링해야 한다고 가정하자. 첫 번째로 네 개를 렌더링하고, 나머지 세 개를 렌더링한다. 두 번째 알파맵 텍스처의 알파 채널은 0 으로 채워지며, 이는 네 번째 텍스처가 방정식에서 취소되는 결과를 낳는다. 당신은 알파맵 텍스처를 설정하고 세 개의 텍스처를 블렌딩하고 렌더링하게 된다. D3DRS_BLEND 와 D3DRS_SRCBLEND 스테이지는 픽셀 쉐이더의 lrp 와 같은 작업을 수행하는데, 이는 두 번째 패스가 첫 번째와 연속적으로 결합되도록 만든다.

     

    데모 프로그램

     

    The demo application uses the two techniques described here to render a texture splatted quad. I decided not to go for a full heightmap to make it as easy as possible to find the key parts in texture splatting. Because of this, the demo is completely fillrate limited. The initial overhead of the pixel shader may cause some video cards to perform worse with it than with its fixed function equivalent, so take the frame rates with a grain of salt. The pixel shader will almost always come out ahead in a more complex scene.

    You can toggle between the fixed function pipeline and the pixel shader through the option in the View menu.

    The textures used are property of nVidia® and are available in their full resolution athttp://developer.nvidia.com/object/IO_TTVol_01.html.

     

    연결 부분의 문제점

     

    텍스처 스플래팅이 실망스러운게 있다면, 그것은 다음의 문제이다 : 두 개의 이웃하는 스플랫이 있을 때, 그것들 사이에 원하지 않는 단절 부분이 생성된다. 그 모양은 이전의 예제 스플랫을 네 번 타일링함으로써 생성될 수 있다.

     

     
    왜 이런 일이 발생하는 것일까? 위의 두 영역(section)의 알파맵을 살펴 보자.

    둘 사이에 공백이 추가되었고 문제가 가시화되었다. 왼쪽에 존재하는 것은 오른쪽에 있는 것의 경계 값을 알지 못한다. 비디오 카드가 자신의 선형 블렌딩을 수행할 때, 그것은 흰색 텍셀 다음에 검은 색 텍셀이 존재하는지의 여부에 대해서 알 방법이 없다. 그것은 경계 부분과 같은 색상이라고 간주한다.

     

    이것은 수정하기 쉬운 문제가 아니며, 많은 게임들이 그것을 방치해 둔다. 그것은 적절한 wrapping 텍스처 및 숙련된 레벨 디자이너에 의해서 감춰질 수 있다. 그러나 좋은 해결책이라 생각치는 않는다. 경험에 의하면 그것은 더 많은 문제를 가지고 있으며, 그것을 해결하는 것이 더 가치가 있다. 그리고 나는 텍스처 스플래팅의 이점이 그 이슈보다 중요하다고 믿는다.

     

    결론

     

    Hopefully this article has cleared up the mystery behind texture splatting. There are, of course, enhancements to be made, but texture splatting in its basic form is a powerful and flexible technique. It creates a smooth blend between different layers of terrain while giving detail at any distance and avoids the patterned look a detail map can give. Its main disadvantage is that it is very fillrate consuming, but with video cards becoming ever more powerful and the abilities of pixel shaders increasing, this is not an issue on modern and future hardware. Its ease of use and flexibility make it a perfect choice for texturing your terrain.

     

    소스

     

    Terrain Texture Compositing by Blending in the Frame-Buffer by Charles Bloom,http://www.cbloom.com/3d/techdocs/splatting.txt

     

    And, of course, the helpful people at http://www.gamedev.net

    Feel free to send any questions or comments to nglasser@charter.net or private message Raloth on the forums!

     

    Discuss this article in the forums


    Date this article was posted to GameDev.net: 4/23/2005 
    (Note that this date does not necessarily correspond to the date the article was written)

    See Also:
    Hardcore Game Programming 

     

    © 1999-2006 Gamedev.net. All rights reserved. Terms of Use Privacy Policy 
    Comments? Questions? Feedback? Click here!

     

    이번에 다룰 내용은 꿈과 희망의 화면좌표 기준으로 2D Sprite 그리기 이다.

    3D API ( OpenGL/ DirectX( Direct3D ) ) 를 이용해 2D 게임을 만들고 싶은 인간이라면

    한번 들여다 봐도 손해보진 않을것이다.

    이론적인 부분은 앞서 [ 3D Programming ] 코너에서

    - http://blog.naver.com/kzh8055/140050063556

    소개해 뒀으니 참고할 인간은 참고 하길 바란다. 사실 별 내용도 없지만서도.

    하여간 바로 소스 코드로 들어간다.

    //----------------------------------------------------------------------------
    // 화면 좌표기준으로 2D Sprite 를 그린다
    //----------------------------------------------------------------------------
    // 아래의 모듈을 별도의 함수로 만드는 것이 편리할것이다.
    // 뭐 가령 DrawSprite( Texture객체 포인터, 위치 좌표 ... ) 식으로 말이다
    //----------------------------------------------------------------------------
    float Sprite2DPosX = 0.0f; // 화면에서의 Sprite X좌표
    float Sprite2DPosY = 0.0f; // 화면에서의 Sprite Y좌표
    float Sprite2DWidth = 200.0f; // Sprite 의 폭( 임시 )
    float Sprite2DHeight = 200.0f; // Sprite 의 높이( 임시 )

    //--------------------------------------------------------
    // 현재 View Port 의 가로, 세로 길이를 얻어온다
    //--------------------------------------------------------
    float ViewPortWidth = 0.0f;
    float ViewPortHeight = 0.0f;

    D3DVIEWPORT9 vp;

    g_pd3dDevice->GetViewport( &vp ); //현재 View Port 정보를 얻어온다

    ViewPortWidth = vp.Width;
    ViewPortHeight = vp.Height;

    //--------------------------------------------------------
    // 화면 좌표를 투영 좌표계로 변환
    //--------------------------------------------------------
    float PosStartX = ( ( 2.0 * Sprite2DPosX ) / ViewPortWidth ) - 1.0f;
    float PosStartY = ( ( -2.0 * Sprite2DPosY ) / ViewPortHeight ) + 1.0f;
    float PosEndX = ( ( 2.0 * ( Sprite2DPosX + Sprite2DWidth ) ) / ViewPortWidth ) - 1.0f;
    float PosEndY = ( ( -2.0 * ( Sprite2DPosY + Sprite2DHeight ) ) / ViewPortHeight ) + 1.0f;

    //-----------------------------
    // 기하 변환 행렬들을 설정
    //-----------------------------
    D3DXMATRIXA16 matWorld; // 월드 변환
    D3DXMATRIXA16 matView; // 뷰 변환
    D3DXMATRIXA16 matOrtho; // 투영 변환( 직교 )

    D3DXMatrixIdentity( &matWorld );
    D3DXMatrixIdentity( &matOrtho );
    D3DXMatrixIdentity( &matView );

    g_pd3dDevice->SetTransform( D3DTS_WORLD, &matWorld );
    g_pd3dDevice->SetTransform( D3DTS_VIEW, &matView );

    //--------------------------------------------------------------
    // 직교 투영 변환 행렬
    //--------------------------------------------------------------
    // 카메라 좌표계의 시야 절두체를
    // ( -1.0f, -1.0f, -1.0f ) ~ ( 1.0f, 1.0f, 0.0f )
    // 범위를 갖는 길이 2.0f인 정육 면체로 만든다.
    //--------------------------------------------------------------
    D3DXMatrixOrthoOffCenterLH( &matOrtho, -1.0f, 1.0f, -1.0f, 1.0f, -1.0f, 0.0f );
    g_pd3dDevice->SetTransform( D3DTS_PROJECTION, &matOrtho );


    //----------------------------------
    // 정점 정의( 투영 좌표계 기준 )
    //----------------------------------
    MYVERTEX Vertices[] =
    {
    { PosStartX, PosStartY, 0.0f, 0, 0 },
    { PosEndX, PosStartY, 0.0f, 1, 0 },
    { PosStartX, PosEndY, 0.0f, 0, 1 },
    { PosEndX, PosEndY, 0.0f, 1, 1 },
    };

    //----------------------------------------------------------------

    // * Sprite를 그리기전에 Z Buffer를 꺼둔다.

    //----------------------------------------------------------------

    // 아니면 Sprite 의 Z 좌표를 다른 3D 객체 보다 앞쪽에 그리는

    // 것도 나쁘지 않을것이다.

    //-----------------------------------------------------------------

    g_pd3dDevice->SetRenderState( D3DRS_ZENABLE, false );
    g_pd3dDevice->SetTexture( 0, g_pTexBillboard[2] ); // Sprite로 그릴 Texture 를 설정한다
    //----------------------------------------
    // Rendering Sprite!!!
    //----------------------------------------
    g_pd3dDevice->DrawPrimitiveUP( D3DPT_TRIANGLESTRIP, 2, Vertices, sizeof(MYVERTEX));

    그럼 이번에는 Quaternion에 의한 회전을 해보도록 하겠습니다.

    Euler의 각도로는 그냥 해당하는 축에 대해서 각도를 늘려주면 되지만

    Quaternion은 어떻게 해야지 회전이 되는걸까요?

    그럼 DirectX에 있는 Quaternion함수를 사용해보겠습니다.

    먼저 앞에서 사용했던 소스에 이번에 비행기를 또 추가합니다.

    위치는 0,0,0인 원점에 놓이게 합니다.

    이정도는 알아서 해주세요. 소스 없습니다.

    그럼 아래와 같은 상태가 될것입니다.

    그럼 현재 상태를 90도 회전하려면 어떻게 해야할까요?

    일단 뭐든지 회전을 하려면 기준축을 정해야하는것은 당연하기때문에

    여러가지 방법을 알아보도록 하겠습니다.

    1.Rotation axis, Rotation angle을 정의하여 회전하는 방법

    Rotation axis = 0,1,0 이어야 하죠? 이유는 당연하구요.

    angle = D3DX_PI/2(90도)

    이렇게 해서 회전된 quaternion을 이용하여 matrix로 변환하여

    회전시키는 방법.

    소스)

    Frame_Move에 아래소스를 추가
    //static int qut_cnt=0;
    D3DXQUATERNION rot_q; //The variable will be used to transformation
    D3DXVECTOR3 rot_axis(0.0f,1.0f,0.0f); //Rotation Axis
    D3DXQuaternionRotationAxis(&rot_q,&rot_axis,D3DX_PI/2);
    D3DXMatrixRotationQuaternion(&g_Obj[17].m_mWorld,&rot_q);
    g_Obj[17].m_mWorld *= 0.1f;
    g_Obj[17].m_mWorld._44 = 1.0f;

    이렇게 하면

    위와 같이 90도 회전한 상태가 된것을 보실 수 있을것입니다.

    그런데 회전방향이 이상하네요. 음... 이것도 왼손으로 90도 돌아간 상태로 나오는군요 역시...

    이넘의 왼손좌표계...

    아무튼 이상은 없습니다.

    그럼 이것을 또한 Animation해보지요.

    소스)

    FrameMove에 추가한 소스를 아래와 같이 수정

    static int qut_cnt=0;
    D3DXQUATERNION rot_q; //The variable will be used to transformation
    D3DXVECTOR3 rot_axis(0.0f,1.0f,0.0f); //Rotation Axis
    D3DXQuaternionRotationAxis(&rot_q,&rot_axis,D3DX_PI/200*qut_cnt++);
    D3DXMatrixRotationQuaternion(&g_Obj[17].m_mWorld,&rot_q);
    g_Obj[17].m_mWorld *= 0.1f;
    g_Obj[17].m_mWorld._44 = 1.0f;

    이렇게 수정하면 count가 계속 올라가면서 Quaternion을 생성하고 또한 이를 Matrix로 변환해서 회전을 하도록 Animation하게

    할 수 있습니다.

    소스는 shadowmap(tutorial-61)-piccoro77.cpp입니다.

    아무튼 이렇게 회전하는 방법이 있을테구요.

    다른방법은로는

    2.그냥 RPY를 이용하는 방법

    static int qut_cnt=0;
    D3DXQUATERNION rot_q; //The variable will be used to transformation
    D3DXQuaternionRotationYawPitchRoll(&rot_q,D3DX_PI/200*qut_cnt++,0.0f,0.0f);
    D3DXMatrixRotationQuaternion(&g_Obj[17].m_mWorld,&rot_q);
    g_Obj[17].m_mWorld *= 0.1f;
    g_Obj[17].m_mWorld._44 = 1.0f;

    참고로 여기서는 Yaw가 Y축, Pitch가 X축, Roll가 X축이랍니다.

    Euler각도는 각도의 순서를 잘 정의해줘야 사용할 수 있으니 주의가 필요하겠죠?

    Soft-Edged Shadows

    by Anirudh.s Shastry

     

    Introduction

     

    원래 동적 그림자 기법은 한정된 방식으로만 가능했다. 그러나 강력한 프로그래밍 가능한 그래픽 하드웨어의 발전과 함께 동적 그림자 기법은 조명 매핑(light mapping) 과 같은 정적 기법이나 투영 그림자(projected shadow) 와 같은 약간 동적인 기법들을 거의 완벽히 대체하게 되었다. 동적 그림자 기법으로 일반적으로 사용되는 것은 그림자 부피(shadow volume)와 그림자 매핑(shadow mapping)이다.

     

    A Closer look

     

    그림자 부피는 닫힌 부피를 생성하기 위해서 조명 방향으로 기하도형을 밀어낸 것(extrusion)을 요구하는 기하도형 기반 기법이다. 그리고 나서 ray casting 을 통해서 씬 내의 그림자가 드리운 부분이 결정될 수 있다(일반적으로 스텐실 버퍼가 ray-casting 을 위해서 사용된다). 이 기법은 픽셀 단위로 정확하며 aliasing 문제를 발생시키지 않지만, 다른 기법들과 마찬가지로 자신만의 단점을 가지고 있다. 이 기법의 두 가지 큰 문제는 그것이 기하도형에 매우 의존적이며 fill-rate 에 민감하다는 것이다. 이 때문에 그림자 매핑이 천천히 대중화되어 가고 있다.

     

    한편 그림자 매핑은 뷰의 조명 위치로부터의 씬 깊이를 렌더링하고 이 깊이 정보를 사용하여 씬 내의 어떠한 위치에 그림자가 드리워져야 하는지를 결정하는 것을 포함하는 이미지 공간 기법이다. 이 기법이 몇 가지 이점을 가지고 있기는 하지만, 그것은 aliasing 인공물과 z-fighting 을 피할 수 없다. 그러나 이에 대한 해결책이 존재하고 장점이 단점보다는 크기 때문에 이것이 이 기사를 위해서 내가 선택하는 기법이 될 것이다.

     

    Soft shadows

     

    딱딱한 그림자는 씬의 사실감을 떨어뜨린다. 그러므로 우리는 씬의 가시 품질을 증가시키기 위해서 부드러운 그림자를 써서 속일 필요가 있다. 매우 열광적인 많은 PHD 학생들이 부드러운 그림자를구현하기 위한 기법을 설명하는 문서를 가지고 왔다. 그러나 이 기법들 중 대부분은 복잡한 씬을 고려했을 때 실시간에 구현하기 어려웠다. 이들 기법들 중 약간의 제약을 극복할 수 있는 하드웨어를 가지게 될 때까지 우리에게는 더 현실적인 기법이 필요할 것이다.

     

    이 기사에서 나는 그림자 맵을 사용해 부드러운 모서리를 가진 그림자를 생성하기 위한 이미지 공간 기법을 제시한다. 이 기법은 완벽한 부드러운 그림자를 생성하지는 않는다(no umbar-penumbra). But it not only solves the aliasing problems of shadow mapping, it improves the visual quality by achieving aesthetically pleasing soft edged shadows.

     

    So how does it work?

     

    먼저 우리는 뷰에서의 조명 위치로부터의 씬 깊이를 부동 소수점 버퍼로 렌더링함으로써 일반적인 그림자 맵을 생성한다. 그리고 나서 그림자를 사용해 씬을 렌더링하는 대신에 그림자가 있는 영역을 스크린 크기의 버퍼로 렌더링한다. 이제 bloom 필터를 사용해 블러링(blur)하고 그것을 스크린 공간에 있는 씬에다가 다시 투영한다. 쉬워 보이는가?

     

    이 기사에서 우리는 집중 조명(spot light)만을 다룬다. 그러나 이 기사는 점 조명(point light)를 다루기 위해서도 쉽게 확장될 수 있다.

     

    여기에 그 단계가 나와 있다 :

     

        - 부동 소수점 버퍼에 씬의 깊이를 씀으로써 일반적인 그림자 맵을 생성한다.

        - 깊이 비교 후에 씬 내의 그림자 영역을 고정 소수점 텍스처에 조명 없이 넣는다.

        - 위의 버퍼를 bloom 필터를 사용해 블러링한다( 이 기사에서는

          sperable Gaussian 필터를 사용하지만, 다른 필터를 사용해도 상관없다).

        - 스크린 공간에서 씬에 블러링된 버퍼를 투영하여 전체 조명을 따라 존재하는

          cool soft-edged 그림자를 획득한다.

     

    Step1 : Rendering the Shadow map

     

    먼저 우리는 씬 깊이를 저장할 수 있는 텍스처를 생성할 필요가 있다. 우리는 이를 렌더 타겟으로 사용할 것이기 때문에, 우리는 텍스처 서피스 데이터를 저장할 서피스도 생성할 필요가 있을 것이다. 그 텍스처는 반드시 부동 소수점이어야 한다. 왜냐하면 깊이 값의 범위가 넓기 때문이다. R32F 포맷은 만족스런 정밀도를 가지고 있으며, 그래서 우리는 그것을 사용할 것이다. 여기에 텍스처를 생성하는 데 사용된 코드가 있다.

     

    // 그림자 맵 생성
    if( FAILED( g_pd3dDevice->CreateTexture( SHADOW_MAP_SIZE,

                                             SHADOW_MAP_SIZE, 1,
                                             D3DUSAGE_RENDERTARGET, D3DFMT_R32F,
                                             D3DPOOL_DEFAULT, &g_pShadowMap,
                                             NULL ) ) )
    {
        MessageBox( g_hWnd, "Unable to create shadow map!",
                   "Error", MB_OK | MB_ICONERROR );
        return E_FAIL;
    }

    // 텍스처의 서피스 획득
    g_pShadowMap->GetSurfaceLevel( 0, &g_pShadowSurf );

     

    이제 그림자 맵을 생성하기 위해서 우리는 씬의 깊이를 그림자 맵에 렌더링할 필요가 있다. 이를 위해서 조명의 world-view-projection 행렬을 사용해 씬을 렌더링해야만 한다. 아래에 그 행렬을 생성하는 방법이 나와 있다.

     

    // 정규 뷰 행렬
    D3DXMatrixLookAtLH( &matView, &vLightPos, &vLightAim, &g_vUp );


    // 조명을 위한 투영 행렬

    D3DXMatrixPerspectiveFovLH( &matProj, D3DXToRadian(30.0f), 1.0f, 1.0f, 1024.0f );


    // 위의 2 행렬을 월드 행렬과 곱하여 원하는 행렬 획득
    matLightViewProj = matWorld * matView * matProj;

     

    아래에 씬 깊이를 렌더링하기 위한 정점 및 픽셀 쉐이더가 나와 있다.

     

    // 그림자 생성 정점 쉐이더


    struct VSOUTPUT_SHADOW
    {
       float4 vPosition    : POSITION;
       float  fDepth       : TEXCOORD0;
    };

    VSOUTPUT_SHADOW VS_Shadow( float4 inPosition : POSITION )
    {
       // 출력 구조체
       VSOUTPUT_SHADOW OUT = (VSOUTPUT_SHADOW)0;
       // 변환된 위치를 출력
       OUT.vPosition = mul( inPosition, g_matLightViewProj );
       // 씬 깊이를 출력
       OUT.fDepth = OUT.vPosition.z;
       return OUT;
    }

     

    여기에서 우리는 조명의 위치에 world-view-projection 행렬(g_matLightViewProj)을 곱했으며, 변환된 위치의 z 값을 깊이로 사용했다. 픽셀 쉐이더에서 우리는 색상값으로 그 깊이를 출력한다.

     

    float4  PS_Shadow( VSOUTPUT_SHADOW IN ) : COLOR0
    {
       // 씬 깊이를 출력한다

       return float4( IN.fDepth, IN.fDepth, IN.fDepth, 1.0f );
    }

     

    자 봐라! 우리는 그림자 맵을 가지게 되었다. 아래에는 그림자의 색상으로 코딩된 버전이 나와 있다. 어두운 파란색은 깊이 값이 낮음을 의미하고, 밝은 파란색은 깊이 값이 높음을 의미한다.

     


     
    Setp 2 : Rendering the Shadowed scene into a buffer
     
    다음으로 씬에서 우리는 그림자가 있는 영역을 오프스크린 버퍼로 렌더링할 필요가 있다. 그럼으로써 우리는 그것을 블러링하고 그것을 다시 씬으로 투영할 수 있다. 그렇게 하기 위해서 먼저 씬 내에서 그림자가 있는 영역을 스크린 크기의 고정 소수점 텍스처에 렌더링한다.
     

    // 스크린 크기 버퍼 맵을 생성
    if( FAILED( g_pd3dDevice->CreateTexture( SCREEN_WIDTH, SCREEN_HEIGHT, 1,
                D3DUSAGE_RENDERTARGET, D3DFMT_A8R8G8B8, D3DPOOL_DEFAULT,

                &g_pScreenMap, NULL ) ) )
    {
       MessageBox( g_hWnd, "Unable to create screen map!",
                   "Error", MB_OK | MB_ICONERROR );
       return E_FAIL;
    }


    // 텍스처의 서피스를 획득
    g_pScreenMap->GetSurfaceLevel( 0, & g_pScreenSurf );

    projective 텍스처 좌표를 얻기 위해서 우리는 "texture" 행렬을 필요로 한다. 이는 투영 공간으로부터의 위치를 텍스처 공간으로 매핑할 것이다.
     
    // 텍스처 행렬 생성
    float fTexOffs = 0.5 + (0.5 / (float)SHADOW_MAP_SIZE);
    D3DXMATRIX matTexAdj( 0.5f,     0.0f,     0.0f, 0.0f,
                          0.0f,     -0.5f,    0.0f, 0.0f,
                          0.0f,     0.0f,     1.0f, 0.0f,
                          fTexOffs, fTexOffs, 0.0f, 1.0f );

    matTexture = matLightViewProj * matTexAdj;
     
    우리는 깊이 비교를 통해 일반적인 그림자 요소를 획득했다. 그러나 완벽하게 조명이 비춰지는 씬을 출력하는 대신에, 우리는 단지 그림자 요소만을 출력한다. 여기에 그 작업을 하기 위한 정점 및 픽셀 쉐이더가 있다.
    // 그림자 매핑 정점 쉐이더 
    struct VSOUTPUT_UNLIT 
    { 
     float4 vPosition : POSITION; 
     float4 vTexCoord : TEXCOORD0; 
     float fDepth : TEXCOORD1; 
    };
     VSOUTPUT_UNLIT VS_Unlit( float4 inPosition : POSITION ) 
    { 
     // 출력 구조체 
     VSOUTPUT_UNLIT OUT = (VSOUTPUT_UNLIT)0; 
     // 변환된 정점 출력 
     OUT.vPosition = mul( inPosition, g_matWorldViewProj ); 
     // 투영 텍스처 좌표 출력 
     OUT.vTexCoord = mul( inPosition, g_matTexture ); 
     // 씬 깊이 출력 
     OUT.fDepth = mul( inPosition, g_matLightViewProj ).z;
     return OUT; 
    } 
    우리는 percentage closer filtering(PCF) 를 사용해 들쑥날쑥한 모서리들을 부드럽게 한다. PCF 를 "수행"하기 위해서는 중심 텍셀을 둘러 싸는 8 개의 주변 텍셀을 샘플링하고(여기에서 우리는 3x3 PCF 커널을 사용한다), 모든 깊이 비교를 이용한다.
     
    // 그림자 매핑 픽셀 쉐이더
    float4  PS_Unlit( VSOUTPUT_UNLIT IN ) : COLOR0
    {
       // 3x3 PCF 커널을 위해서 9 개의 좌표를 생성
       float4 vTexCoords[9];
       // 텍셀 크기
       float fTexelSize = 1.0f / 1024.0f;

       
    // 지정된 깊이 맵 크기를 위한 텍스처 좌표를 생성
       // 4 3 5
       // 1 0 2
       // 7 6 8

       vTexCoords[0] = IN.vTexCoord;
       vTexCoords[1] = IN.vTexCoord + float4( -fTexelSize, 0.0f, 0.0f, 0.0f );
       vTexCoords[2] = IN.vTexCoord + float4(  fTexelSize, 0.0f, 0.0f, 0.0f );
       vTexCoords[3] = IN.vTexCoord + float4( 0.0f, -fTexelSize, 0.0f, 0.0f );
       vTexCoords[6] = IN.vTexCoord + float4( 0.0f,  fTexelSize, 0.0f, 0.0f );
       vTexCoords[4] = IN.vTexCoord + float4( -fTexelSize, -fTexelSize, 0.0f, 0.0f );
       vTexCoords[5] = IN.vTexCoord + float4(  fTexelSize, -fTexelSize, 0.0f, 0.0f );
       vTexCoords[7] = IN.vTexCoord + float4( -fTexelSize,  fTexelSize, 0.0f, 0.0f );
       vTexCoords[8] = IN.vTexCoord + float4(  fTexelSize,  fTexelSize, 0.0f, 0.0f );

       // 테스트하는 픽셀이 그림자 영역에 있는지를 검사하면서 샘플링
       float fShadowTerms[9];
       float fShadowTerm = 0.0f;
       forint i = 0; i < 9; i++ )
       {
          float A = tex2Dproj( ShadowSampler, vTexCoords[i] ).r;
          float B = (IN.fDepth ?0.1f);

          // 그림자 영역의 텍셀
          fShadowTerms[i] = A < B ? 0.0f : 1.0f;
          fShadowTerm     += fShadowTerms[i];
       }
       // 평균 획득
       fShadowTerm /= 9.0f;
       return fShadowTerm;
    }
     
    이 스크린 버퍼는 썩 괜찮다! 이제 이것을 블러링하고 다시 스크린 공간의 씬에 투영할 필요가 있다.
     

     
    Step 3 : Blurring the screen buffer
     
    우리는 seperable gaussian 필터를 사용해서 스크린 버퍼를 블러링하지만, Poisson 필터를 사용할 수도 있다. 현재의 렌더 타겟은 해당 서피스에 연결된 A8R8G8B8 텍스처이다. 우리는 2 개의 렌더 타겟을 필요로 하는데, 하나는 수직 경로를 위한 것이고 다른 하나는 수평 경로를 위한 것이다.
     
    // 블러 맵 생성
    for( int i = 0; i < 2; i++ )
    {
       if( FAILED( g_pd3dDevice->CreateTexture( SCREEN_WIDTH, SCREEN_HEIGHT, 1,
                                                D3DUSAGE_RENDERTARGET,
                                                D3DFMT_A8R8G8B8, D3DPOOL_DEFAULT,
                                                &g_pBlurMap[i], NULL ) ) )
       {
          MessageBox( g_hWnd, "Unable to create blur map!",
                      "Error", MB_OK | MB_ICONERROR );
          return E_FAIL;
       }
      // 텍스처 서피스 획득
       g_pBlurMap[i]->GetSurfaceLevel( 0, & g_pBlurSurf[i] );
    }
     
    우리는 15 개의 Gaussian 오프셋 및 관련 가중치를 다음 함수들을 사용해 생성했다.
     
    float GetGaussianDistribution( float x, float y, float rho )
    {
       float g = 1.0f / sqrt( 2.0f * 3.141592654f * rho * rho );
       return g * exp( -(x * x + y * y) / (2 * rho * rho) );
    }
     
    void GetGaussianOffsets( bool bHorizontal, D3DXVECTOR2 vViewportTexelSize,
                             D3DXVECTOR2* vSampleOffsets, float* fSampleWeights )
    {
       // 중심텍셀 오프셋과 가중치 획득
       fSampleWeights[0] = 1.0f * GetGaussianDistribution( 0, 0, 2.0f );
       vSampleOffsets[0] = D3DXVECTOR2( 0.0f, 0.0f );
       // 남은 요소들의 오프셋 및 가중치 획득
       if( bHorizontal )
       {
          for( int i = 1; i < 15; i += 2 )
          {
             vSampleOffsets[i + 0] = D3DXVECTOR2( i * vViewportTexelSize.x, 0.0f );
             vSampleOffsets[i + 1] = D3DXVECTOR2( -i * vViewportTexelSize.x, 0.0f );
             fSampleWeights[i + 0] = 2.0f * GetGaussianDistribution( float(i + 0), 0.0f, 3.0f );
             fSampleWeights[i + 1] = 2.0f * GetGaussianDistribution( float(i + 1), 0.0f, 3.0f );
          }
       }
       else 
       {
          for( int i = 1; i < 15; i += 2 )
          {
             vSampleOffsets[i + 0] = D3DXVECTOR2( 0.0f, i * vViewportTexelSize.y );
             vSampleOffsets[i + 1] = D3DXVECTOR2( 0.0f, -i * vViewportTexelSize.y );
             fSampleWeights[i + 0] = 2.0f * GetGaussianDistribution( 0.0f, float(i + 0), 3.0f );
             fSampleWeights[i + 1] = 2.0f * GetGaussianDistribution( 0.0f, float(i + 1), 3.0f );
          }
       }
    }
     
    스크린 버퍼를 블러링하기 위해서 블러 맵을 렌더 타겟으로 설정하고, 스크린 크기의 쿼드를 다음 정점 및 픽셀 쉐이더를 사용해 렌더링한다.
     

    // Gaussian 필터 정점 쉐이더
    struct VSOUTPUT_BLUR
    {
       float4 vPosition    : POSITION;
       float2 vTexCoord    : TEXCOORD0;
    };

     

    VSOUTPUT_BLUR VS_Blur( float4 inPosition : POSITION, float2 inTexCoord : TEXCOORD0 )
    {
       // 출력 구조체
       VSOUTPUT_BLUR OUT = (VSOUTPUT_BLUR)0;
       // Output the position
       OUT.vPosition = inPosition;
       // Output the texture coordinates
       OUT.vTexCoord = inTexCoord;
       return OUT;
    }


    // 수평 블러 픽셀 쉐이더
    float4 PS_BlurH( VSOUTPUT_BLUR IN ): COLOR0
    {
       // Accumulated color
       float4 vAccum = float4( 0.0f, 0.0f, 0.0f, 0.0f );
       // Sample the taps (g_vSampleOffsets holds the texel offsets
       // and g_fSampleWeights holds the texel weights)
       for(int i = 0; i < 15; i++ )
       {
          vAccum += tex2D( ScreenSampler, IN.vTexCoord + g_vSampleOffsets[i] ) * g_fSampleWeights[i];
       }
       return vAccum;
    }


    // 수직 블러 픽셀 쉐이더
    float4 PS_BlurV( VSOUTPUT_BLUR IN ): COLOR0
    {
       // Accumulated color
       float4 vAccum = float4( 0.0f, 0.0f, 0.0f, 0.0f );
       // Sample the taps (g_vSampleOffsets holds the texel offsets and
       // g_fSampleWeights holds the texel weights)
       for( int i = 0; i < 15; i++ )
       {
          vAccum += tex2D( BlurHSampler, IN.vTexCoord + g_vSampleOffsets[i] ) * g_fSampleWeights[i];
       }
       return vAccum;
    }

    이제 블러 맵이 준비되었다. 그림자의 블러 양을 늘리려면 텍셀 샘플링 거리를 증가시키면 된다. 마지막 단계는 블러링된 맵을 스크린 공간의 씬에 다시 투영하는 것이다.
     
     

    첫 번째 Gaussian 패스 이후

    두 번째 Gaussian 패스 이후
     
    Step 4 : Rendering the shadowed scene
     
    블러 맵을 씬에 투영하기 위해서 우리는 씬을 보통과 같이 렌더링한다. 그러나 블러 맵을 스크린 공간 좌표를 사용해 투영한다. 우리는 약간의 하드 코딩된 math 를 사용하는 clip 공간 위치를 사용해 스크린 공간 좌표를 생성한다. 아래에 그림자를 따라 픽셀당 조명을 사용하여 씬을 렌더링하는 정점 및 픽셀 쉐이더가 나와 있다.
     
    struct VSOUTPUT_SCENE
    {
       float4 vPosition      : POSITION;
       float2 vTexCoord      : TEXCOORD0;
       float4 vProjCoord     : TEXCOORD1;
       float4 vScreenCoord   : TEXCOORD2;
       float3 vNormal        : TEXCOORD3;
       float3 vLightVec      : TEXCOORD4;
       float3 vEyeVec        : TEXCOORD5;
    };

    // 씬 정점 쉐이더
    VSOUTPUT_SCENE VS_Scene( float4 inPosition : POSITION, float3 inNormal : NORMAL,
                             float2 inTexCoord : TEXCOORD0 )
    {
       VSOUTPUT_SCENE OUT = (VSOUTPUT_SCENE)0;
       // 변환된 위치 출력
       OUT.vPosition = mul( inPosition, g_matWorldViewProj );
       // 텍스처 좌표 출력
       OUT.vTexCoord = inTexCoord;
       // 투영 텍스처 좌표 출력
       // (spot 텍스처를 씬에 투영하기 위해 이를 사용)
       OUT.vProjCoord = mul( inPosition, g_matTexture );
       // 스크린 공간 텍스처 좌표 출력
       OUT.vScreenCoord.x = ( OUT.vPosition.x * 0.5 + OUT.vPosition.w * 0.5 );
       OUT.vScreenCoord.y = ( OUT.vPosition.w * 0.5 - OUT.vPosition.y * 0.5 );
       OUT.vScreenCoord.z = OUT.vPosition.w;
       OUT.vScreenCoord.w = OUT.vPosition.w;
       // 월드 공간 정점 위치 획득
       float4 vWorldPos = mul( inPosition, g_matWorld );
       // 월드 공간 법선 출력
       OUT.vNormal = mul( inNormal, g_matWorldIT );
       // 조명 벡터를 탄젠트 공간으로 이동시킴
       OUT.vLightVec = g_vLightPos.xyz - vWorldPos.xyz;
       // 관찰자 위치(eye) 벡터를 탄젠트 공간으로 이동시킴
       OUT.vEyeVec = g_vEyePos.xyz - vWorldPos.xyz;
       return OUT;
    }
     
    우리는 조명으로부터 sopt 텍스처를 투영하기 위해서 부가적인 spot 항을 추가했다. 이것은 집중 조명 효과를 시뮬레이션하기 위한 목적 뿐만 아니라, 그림자 맵 외부의 씬의 바깥쪽 영역을 잘라내기 위한 목적도 있다. 이 spot 맵은 표준 투영 텍스처링을 사용해서 투영된다.
     

    float4 PS_Scene( VSOUTPUT_SCENE IN ) : COLOR0
    {
       // 법선, 광원, 눈 벡터 정규화
       IN.vNormal   = normalize( IN.vNormal );
       IN.vLightVec = normalize( IN.vLightVec );
       IN.vEyeVec   = normalize( IN.vEyeVec );


       // 색상 및 법선 맵 샘플링
       float4 vColor  = tex2D( ColorSampler, IN.vTexCoord );
       // 주변, 분산, 반사 조명 항 계산
       float ambient  = 0.0f;
       float diffuse  = max( dot( IN.vNormal, IN.vLightVec ), 0 );
       float specular = pow(max(dot( 2 * dot( IN.vNormal, IN.vLightVec ) * IN.vNormal
                                     - IN.vLightVec, IN.vEyeVec ), 0 ), 8 );
       if( diffuse == 0 ) specular = 0;
       // 그림자 항 획득
       float fShadowTerm = tex2Dproj( BlurVSampler, IN.vScreenCoord );
       // spot 항 획득
       float fSpotTerm = tex2Dproj( SpotSampler, IN.vProjCoord );
       // 최종 색상 계산
       return (ambient * vColor) +
              (diffuse * vColor * g_vLightColor * fShadowTerm * fSpotTerm) +
              (specular * vColor * g_vLightColor.a * fShadowTerm * fSpotTerm);
    }

    바로 이것이다! 우리는 매우 괜찮아 보이는 부드러운 모서리 그림자를 가지게 되었다. 이 기법의 이점은 완벽하게 그림자 맵의 단점인 모서리 aliasing 인공물을 제거한다는 것이다.  다른 이점은 적은 메모리 오버헤드를 가지고 다중 조명을 위한 부드러운 그림자를 생성할 수 있다는 것이다. 다중 조명을 다룰 때 당신이 할 일은 조명당 그림자 맵을 하나씩 만들어 주는 것 뿐이다. 반면에 스크린이나 블러 버퍼는 모든 조명에 의해 공유될 수 있다. 마지막으로 이 기법은 그림자 맵 및 그림자 부피 모두에 적용될 수 있다. 그래서 그림자 기법에 상관없이 당신은 이 기법을 사용해 부드러운 모서리 그림자를 생성할 수 있다. 단점이 하나 있다면 이 기법은 Gaussian 필터를 사용하기 위해서 fill-rate 에 상당히 민감하다는 것이다. 이것은 작은 블러 버퍼를 사용하거나 가시 품질을 약간 희생함으로써 최소화될 수 있다.
     
    다음은 여기에서 언급한 접근들 사이의 비교이다. 3x3 percent closer  필터링 및 일반 그림자 매핑이다.
     

     
    내 기사를 읽어주어서 감사를 표한다. 당신이 좋아했으면 하는 바램이다. 만약 의구심, 질문, 덧글이 있다면anidex@yahoo.com 으로 편하게 메일 보내기 바란다. 소스코드는http://downloads.gamedev.net/features/hardcore/softedgeshadow/SoftShadows.zip 에 있다.
     
    Reference
     
    - Hardware Shadow Mapping. Cass Everitt, Ashu Rege and Cem Cebenoyan
    - Hardware-accelerated Renderng of Antialiased Shadows with Shadow Map. Stefan Brabec and Hans-Peter Seidel.
     

    Discuss this article in the forums


    Date this article was posted to GameDev.net: 1/18/2005 
    (Note that this date does not necessarily correspond to the date the article was written)

     

    See Also:


    Hardcore Game Programming 
    Shadows 

     

    © 1999-2006 Gamedev.net. All rights reserved. Terms of Use Privacy Policy 
    Comments? Questions? Feedback? Click here!

    + Recent posts