728x90

오늘은 오랜만에 재미있는 3D 그래픽에 대해서 이야기 해보겠습니다. 주제는 Bloom Effect, 블룸 효과 입니다.

 

먼저 블룸 효과에 대해서 간단히 이해해 보자면,

 

빛을 받는 물체는 그 주위에 빛을 발산하는 것과 같은 효과를 나타냅니다.

 

뭐 예를 들면, 태양이 쩅쨍 찌는 사막의 한가운데서 먼 사막의 모래 산등성이를 바라보면 마치 모래 산등성이가 태양 빛을 반사하여 희미하게 빛을 발산하는 것처럼 보이는 풍경을 상상할 수 있을 겁니다.

 

상상이 안되면 사진을 직접 보죠. 뭐.  ( 진작에 보여줘야 했었나?  ㅋ )

 


 -블룸 효과 미적용(왼쪽) : 블룸 효과 적용(오른쪽)

이미지 출처: http://cafe.naver.com/raisonblue/379 -

 

 확연히 차이가 나죠. 오른쪽 같은 경우는 빛이 은은하게 퍼지는 것처럼 보입니다. 따라서 블룸 효과를 일종 뽀샤시 효과라고도 합니다만

뭐, 최근 3D 게임 같은 경우는 블룸 효과를 대부분 지원하고 엘더스크롤과 같은 좀 고급 게임은 HDR 효과까지 지원하더군요. HDR 효과는 여기서 논할 거리가 아니기에 생략.

 

자, 이제 블룸 효과가 뭔지는 이해했을 겁니다. 바로 구현 들어가 보죠 -_-

 

그런데 블룸 효과 적용에 앞서 2차원 가우스 분포 함수에 대해서 공부해 볼 필요가 있습니다. 가우스 분포 함수를 이용하면 번짐 효

과를 구현할 수 있기 때문이죠.

 

1차원 가우스 분포 함수의 식이 다음과 같으며

 


 

여기서 б 표준 편차입니다. 표준 편차가 1일 때 1차원 가우스 분포 함수는 다음과 같은 모양을 갖죠

 


 

그래프 모양을 봐선 뭔가 좌우 대칭으로 증폭이 됨을 알 수 있습니다. 즉, 빛을 발산하는 색상이 증폭이 되는 것이며 화면은 x, y 2차원이기 때문에 2차원 가우스 분포 함수를 사용해야 하는 것이죠.

 



-2차원 가우스 분포 함수 모양-

 

식에서 설명하지 않았던 상수 부분  W(x) = 

에서 x가 б 이면 pow( e, 0.5 ) 와 같은 결과므로 대략 0.6065가 됩니다. 이걸로 얻은  W(x)로 가중치 값을 얻어내서 이 값들을 마스크로 사용하면 됩니다.

 

б 는 우리가 임의의 상수로 결정할 수 있으며 그에 따른 마스크 값들을 미리 구해 놓을 수 있습니다.
 
그리고 마스크 적용은 픽셀 셰이더가 수행하면 되겠죠.
 
구현 방안에 있어선, 일단 기존 화면의 렌더링한 결과를 텍스처로 저장한 후에 이 텍스처를 새로 추가한 픽셀 셰이더를 적용하고 화면 사이즈에 맞게 뿌려주면 되는 것입니다. 렌더링 패스도 두 번에 끝나는 매우 저렴한 결과를 보여주죠. ㅋ_ㅋ
 
다음은 블룸 효과 헤더 부분입니다. 다이렉트 x를 사용합니다.

  class BloomResource {


  LPDIRECT3DSURFACE9       lpD3DSurface[ 2];                             //다이렉트 표면 장치
  LPDIRECT3DTEXTURE9       lpD3DTexture[ 2];                             //다이렉트 텍스처 장치

  LPDIRECT3DVERTEXBUFFER9     lpD3DTexVB;                           //텍스처 정점 버텍스

  LPD3DXEFFECT                         lpD3DEffect;                             //다이렉트 이펙트 핸들
  D3DXHANDLE                             hTechBloom;                          //셰이더 테크닉 핸들


  BOOL InitBloomResource( LPDIRECT3DDEVICE9  );
  VOID RenderBloom( LPDIRECT3DDEVICE9 );

 

 };

 
핵심은, 다이렉트 서피스와 텍스처를 각각 2개씩 생성한다는 점이고 그 이유는 다음과 같습니다. 원래 기본 화면을 렌더링 한 후, 그 렌더링 한 결과를 1번 서피스에 저장하고 1번 서피스에 블룸 효과를 적용시켜서 2번에 저장한 후에 1번과 2번의 화면을 합성시키는 것이죠.
따라서, 각 서피스의 결과를 저장할 텍스처 2개를 따로 생성하고 있으며 이 텍스처를 화면에 뿌려주어야 하기 때문에 텍스처 정점 버텍스 ( FVF = D3DFVF_XYZRHW | D3DFVF_TEX  ) 도 선언하고 있으며 픽셀 셰이더 적용을 위한 셰이더 테크닉 핸들도 선언하고 있습니다.
 
그럼, InitBloomResource 함수를 보도록 하죠.
 

 BOOL BloomResource ::InitBloomResource( LPDIRECT3DDEVICE9 lpD3DDevice ) {

 

 //스크린 사이즈 구하기 
 POINT ptScreen;
 ptScreen.x = d3ddm.Width;
 ptScreen.y = d3ddm.Height;

 

 //텍스처 정점 설정
 FTexture vertices[ 4 ];

 vertices[ 0 ].vPosition = D3DXVECTOR4(  ptScreen.x - 0.5f, ptScreen.y - 0.5f, 0, 1 );
 vertices[ 0 ].vTexCoord = D3DXVECTOR2( 1, 1 );
 vertices[ 1 ].vPosition = D3DXVECTOR4( -0.5f, ptScreen.y - 0.5f, 0, 1 );
 vertices[ 1 ].vTexCoord = D3DXVECTOR2( 0, 1 );
 vertices[ 2 ].vPosition = D3DXVECTOR4( -0.5f, -0.5f, 0, 1 );
 vertices[ 2 ].vTexCoord = D3DXVECTOR2( 0, 0 );
 vertices[ 3 ].vPosition = D3DXVECTOR4( ptScreen.x - 0.5f, -0.5f, 0, 1 );
 vertices[ 3 ].vTexCoord = D3DXVECTOR2( 1, 0 );

 

 HRESULT  hResult = E_FAIL;

 UINT vertexSize = sizeof( vertices );

 hResult = lpD3DDevice->CreateVertexBuffer(  vertexSize,  0,  vertices[ 0 ].FVF,  D3DPOOL_DEFAULT,   &lpD3DTexVB,  NULL );

 

 if( FAILED( hResult ) ) {   return FALSE; }

 

VOID* pVertices = NULL;

 

 hResult = lpD3DTexVB->Lock( 0, vertexSize, static_cast< VOID** >( &pVertices ), 0 );

 

 if( FAILED( hResult ) ) {  return FALSE; }

 

 memcpy( pVertices, vertices, vertexSize );

 

 lpD3DTexVB->Unlock();


 //셰이더 설정
 hResult = D3DXCreateEffectFromFile( lpD3DDevice,   szFileName,  NULL,  NULL,  D3DXSHADER_DEBUG,  NULL,  &lpD3DEffect,  NULL );

 

 hTechBloom = lpD3DEffect->GetTechniqueByName( "GOneBloom" );

 FLOAT fTexSizeX = static_cast< FLOAT >( ptScreen.x );
 FLOAT fTexSizeY = static_cast< FLOAT >( ptScreen.y );

 D3DXHANDLE hMapCX = lpD3DEffect->GetParameterByName( NULL, "map_cx" );
 lpD3DEffect->SetValue( hMapCX, &fTexSizeX, sizeof( FLOAT ) );

 D3DXHANDLE hMapCY = lpD3DEffect->GetParameterByName( NULL, "map_cy" );
 lpD3DEffect->SetValue( hMapCY, &fTexSizeY, sizeof( FLOAT ) );

 

//서피스 및 텍스처 생성 

D3DXCreateTexture(lpD3DDevice,  TEXTURE_SIZE, TEXTURE_SIZE, 0, D3DUSAGE_RENDERTARGET, d3ddm.Format, D3DPOOL_DEFAULT, &lpD3DTexture[ 0 ] );

lpD3DTexture[ 0 ]->GetSurfaceLevel( 0, &lpD3DSurface[ 0 ] );


 D3DXCreateTexture(lpD3DDevice,  ptScreen.x,  ptScreen.y, 0, D3DUSAGE_RENDERTARGET, d3ddm.Format,

D3DPOOL_DEFAULT, &lpD3DTexture[ 1 ] );

 lpD3DTexture[ 1 ]->GetSurfaceLevel( 0, &lpD3DSurface[ 1 ] );

 

return TRUE;

 

}//BOOL InitBloomResource( LPDIRECT3DDEVICE9 lpD3DDevice )

 
초기화 같은 경우는 별로 할 일이 없습니다.
 
화면 사이즈를 구하고
최종 결과물을 화면에 뿌려주기 위해 정점 설정을 하고 ( 시점 변환을 생략하기 위해서 RHW4 포멧의 정점임을 주의할 것! )
셰이더 설정을 하고
서피스와 텍스처를 생성합니다.
 
TEXTURE_SIZE 같은 경우는 실제 화면 해상도 이하로 잡으면 됩니다. 물론 클수록 화면 퀄리티가 좋으나 성능 부하가 커지겠죠. 저 같은 경우는 500 으로 설정했습니다.
 
그럼 렌더링 부분을 보도록 하죠.
 

 VOID BloomResource ::RenderBloom( LPDIRECT3DDEVICE9  lpD3DDevice ) {
 

 LPDIRECT3DSURFACE9       lpCurrentSurface = NULL;     //현재 화면 장치 
 lpD3DDevice->GetRenderTarget( 0, &lpCurrentSurface );
 lpD3DDevice->StretchRect(  lpCurrentSurface, NULL, lpD3DSurface[ 0 ], NULL, D3DTEXF_LINEAR );
 lpD3DDevice->StretchRect(  lpCurrentSurface, NULL, lpD3DSurface[ 1 ], NULL, D3DTEXF_LINEAR );

 lpD3DDevice->SetRenderTarget( 0, lpCurrentSurface );
 SAFE_RELEASE( lpCurrentSurface ); 

 

 lpD3DDevice->SetFVF( FTexture ::FVF );
 lpD3DDevice->SetTextureStageState( 0, D3DTSS_COLORARG1, D3DTA_TEXTURE );
 lpD3DDevice->SetTextureStageState( 0, D3DTSS_COLOROP, D3DTOP_SELECTARG1 );
 lpD3DDevice->SetTexture( 0, lpD3DTexture[ 0 ]);
 lpD3DDevice->SetTexture( 1, lpD3DTexture[ 1 ]);

 

 lpD3DEffect->SetTechnique( hTechBloom );

 UINT iPass = 0;

 lpD3DEffect->Begin( &iPass,  NULL  );
 lpD3DEffect->BeginPass( 0 );
 {
  lpD3DDevice->Clear( 0, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB( 255, 255, 255 ), 1.0f, 0 );
  lpD3DDevice->SetStreamSource( 0, lpD3DTexVB, 0, sizeof( FTexture ) );
  lpD3DDevice->DrawPrimitive( D3DPT_TRIANGLEFAN, 0, 2 );
 
  lpD3DEffect->EndPass();

 }//IF

 

 lpD3DEffect->End();
 
 lpD3DDevice->SetTexture( 0, NULL );
 lpD3DDevice->SetTexture( 1, NULL );

}

 
빨간 색 소스 부분을 보시면 알겠지만 핵심은 기존의 렌더링한 결과를 0번 서피스에 저장한 후에 이를 블룸 효과 적용해서 1번 서피스에 그려줍니다. 그리곤 0번과 1번의 결과물을 합성해서 화면에 뿌려주면 되죠. 그래서 RenderBloom 함수 같은 경우는 기존 모델들을 그린 후에 최종적으로 호출해 주면 됩니다. 의사 코드는 다음과 같겠죠.
 

VOID Render() {

 

  LPDIRECT3DDEVICE9 lpD3DDevice = m_lpD3DDevice;

 

  //카메라 변환에 의한 뷰행렬 설정

 

  //기본 화면 렌더링 
  if( SUCCEEDED( lpD3DDevice->BeginScene() ) ) {

     lpD3DDevice->Clear( 0, NULL, D3DCLEAR_TARGET | D3DCLEAR_STENCIL | D3DCLEAR_ZBUFFER, D3DCOLOR_XRGB( 

                                 255, 255, 255 ), 1.0f, 0 );


      //여기서 화면에 뿌리고 싶은 모델들 렌더링

 

      lpD3DDevice->EndScene();
  }//if

 

   //블룸 효과를 마지막 렌더링 m_pBloomResource는 BloomResource 객체.
   m_pBloomResource->RenderBloom( lpD3DDevice );

 

   lpD3DDevice->Present( NULL, NULL, m_hWnd, NULL );


}

 
의심할 여지가 없네요. 다음은 실제 블룸 효과 셰이더 소스를 보도록 합시다.
 

float map_cx;                        //텍스처 크기  가로 
float map_cy;                        //텍스처 크기  세로

sampler Sampler0;
sampler Sampler1;


float2 rcpres = { 0.0025, 0.0033333333333333333333333333333333 };

 

//텍스처 의 임의의 위치의 픽셀에 접근하귀 위한 좌표 ( 가로 방향)
float2 PixelKernelH[13] =
{
    { -6, 0 },    { -5, 0 },    { -4, 0 },    { -3, 0 },    { -2, 0 },    { -1, 0 },    {  0, 0 },    {  1, 0 },    {  2, 0 },    {  3, 0 },    {  4, 0 },
    {  5, 0 },    {  6, 0 },
};

 

//텍스처의 임의의 위치의 픽셀에 접근하기 위한 좌표 ( 세로 방향)

float2 PixelKernelV[13] =
{
    { 0, -6 },    { 0, -5 },    { 0, -4 },    { 0, -3 },    { 0, -2 },    { 0, -1 },    { 0,  0 },    { 0,  1 },    { 0,  2 },    { 0,  3 },    { 0,  4 },
    { 0,  5 },    { 0,  6 },
};

 

//미리 계산해 둔 가우스 필터의 마스크 값
float BlurWeights[13] = 
{
    0.002216,    0.008764,    0.026995,    0.064759,    0.120985,    0.176033,    0.199471,    0.176033,    0.120985,    0.064759,
    0.026995,    0.008764,    0.002216,

};

 

 

float4 PSBlur( float2 Tex : TEXCOORD ) : COLOR {

 

    float4 Color = tex2D( Sampler0, Tex );
    Color = pow( Color, 32 );
    
    float4 Color2 = -0.84;
 
    for( int index = 0; index < 13; ++index ) {
        Color2 += tex2D( Sampler0, Tex + ( PixelKernelH[ index ] * rcpres ) ) * BlurWeights[ index ];
        Color2 += tex2D( Sampler0, Tex + ( PixelKernelV[ index ] * rcpres ) ) * BlurWeights[ index ];

    }
    
    Color2 *= 0.48;
    
    float4 Color3 = tex2D( Sampler1, Tex );

    return  Color + Color2 + Color3;
    
}

 

 

technique GOneBloom {

    pass P0 {
        CullMode = CCW;      
        PixelShader = compile ps_3_0 PSBlur();
    
    }
}

 
상수는 가우스 분포 함수 부분에서 설명했던 것 처럼 미리 계산해 두었으며( 저는 레지던트 이블4 패치 파일에서 참고함) 좌, 우, 상, 하 방향에 대해 픽셀 6칸씩 블러 효과를 적용하고 있습니다. 그 부분이 바로 빨간색 소스 부분이며 최종적으로 기존 화면(Color3)과 블룸효과를 적용한 화면을(Color1 + Color2) 합성해서 화면에 뿌려주죠. 상수 값은 변화시켜보면서 적절한 결과물을 찾아보세요. 정답은 없으니깐.
 
다음은 블룸 효과 적용 전과 후의 결과 입니다.
 
 


-블룸 효과 적용 전-

 
 


-블룸 효과 적용 후-



출처 : http://blog.naver.com/hermet/58261272

728x90

+ Recent posts