2010. 4. 9. 03:34

RenderTarget을 활용한 텍스쳐 스플래팅


지형의 텍스쳐 스플래팅을 프로그래밍하다가 어떤 문제에 부딪혔습니다.
텍스쳐 스플래팅의 경우 텍스쳐의 개수만큼 nPass렌더링을 시도하고 있는데요.
여기서 스플래팅이 완료된 지형에 또 다른 처리를 하고 싶을때 어떻게 해야 하는가에 대한 문제였습니다.
이를테면 스플래팅을 완료한 지형에 조명셰이더를 추가한다던지 하는 문제가 있겠죠. 그렇다면, 스플래팅이 완료된 부분의 텍스쳐를 셰이더에 넘겨줘야하는데요. 셰이더에서는 프레임버퍼로 접근할 수가 없습니다.. 그래서
곰곰이 해결책을 생각해본 결과 해당 타일의 크기만큼(3D세계의 크기) 빈텍스쳐를 생성한 후, 그곳을 렌더타겟을 둬서 렌더링한 후, 그 텍스쳐를 셰이더에 넘겨주면 되겠다 싶어서 그대로 실행해보았습니다. 결과는 성공적이었습니다. 하지만 어떤면에선 실패였습니다. (이건 아래)

렌더텍스쳐에 관한 변수가 다음과 같이 있습니다.
LPDIRECT3DTEXTURE9   m_pRTTexture = NULL;
LPDIRECT3DTEXTURE9   m_pRTBackBufferTexture = NULL;
LPDIRECT3DSURFACE9   m_pRTBackBuffer = NULL;
LPDIRECT3DSURFACE9   m_pRTDepthBuffer = NULL;
LPDIRECT3DVERTEXBUFFER9  m_pRTVB = NULL;

D3DXMATRIX       m_matRTView;
D3DXMATRIX       m_matRTProj;
D3DVIEWPORT9   m_RTViewport = { 0, 0, TILE_WIDTH, TILE_HEIGHT, 0, 1 };

여기에서 m_matRTView, m_matRTProj, m_RTViewport 정도는 static으로 함수내부에 선언해도 무방합니다. 항상 고정적인 값을 가지기 때문입니다.
위부터 설명해본다면,
m_pRTTexture : 테스트용 텍스쳐

m_pRTBackBufferTexture : 백버퍼의 서피스
m_pRTBackBuffer : 백버퍼 인터페이스
m_pRTDepthBuffer : 깊이버퍼
m_pRTVB : 테스트용 텍스쳐를 렌더링할 사각형(정점이 4개입니다, 삼각형 2개)
m_matRTView : 전용 뷰행렬
m_matRTProj : 전용 프로젝션행렬
m_RTViewport : 전용 뷰포트
여기에서 뷰포트값은 0, 0, TILE_WIDTH, TILE_HEIGHT, 0, 1로 맞춰주셔야 합니다. 흔히 하는 프로그래밍 방식으로, 하나의 타일이 몇개의 셀을 가지고 있고, 셀은 어떤 고정된 크기를 가진다면... 셀가로개수*셀가로크기가 하나의 타일 가로크기가 됩니다. 세로크기도 비슷하게 구해주시면되고요.

아래는 관련변수 초기화 부분입니다.
// 렌더타겟에 쏠 텍스쳐를 가져온다.
 if( D3DXCreateTextureFromFile( m_pd3dDevice, "texture.bmp", &m_pRTTexture ) )
  return E_FAIL;
 // 렌더타겟의 렌더링되는 텍스쳐
 if( FAILED( m_pd3dDevice->CreateTexture( TILE_WIDTH, TILE_HEIGHT, 1, D3DUSAGE_RENDERTARGET,  D3DFMT_A8R8G8B8, D3DPOOL_DEFAULT, &m_pRTBackBufferTexture, NULL ) ) )
  return E_FAIL;
 // 백버퍼를 텍스쳐와 연결한다.
 if( FAILED( m_pRTBackBufferTexture->GetSurfaceLevel( 0, &m_pRTBackBuffer ) ) )
  return E_FAIL;
 // 깊이버퍼를 하나 만든다.
 if( FAILED( m_pd3dDevice->CreateDepthStencilSurface( TILE_WIDTH, TILE_HEIGHT,
  D3DFMT_D24S8, D3DMULTISAMPLE_NONE, 0, TRUE, &m_pRTDepthBuffer, NULL ) ) )
  return E_FAIL;
하지만 이 부분에서 큰 오류가 있습니다. 이것을 이대로 그대로 따르시면 엄청난 성능저하를 가져오시게 됩니다. 그 이유는 타일의 크기에 따른 생성되는 이미지의 크기가 문제가 되겠죠. 메모리면에선 조금 치명적인 부분입니다.

아래는 테스트용 버텍스버퍼 초기화부분입니다. 타일이 셀이 하나도 없을경우 이렇게 그냥 하시면 되겠죠. 하하.. 일반적으로 그냥 타일을 렌더링해주시면 되므로 실제하실땐 아래부분이 필요없으십니다.
TexVertex rtVertices[] =
{
 { D3DXVECTOR3( -TILE_WIDTH/2, TILE_HEIGHT/2, 0 ),  D3DXVECTOR2(0,0) },
 { D3DXVECTOR3( TILE_WIDTH/2, TILE_HEIGHT/2, 0 ),   D3DXVECTOR2(1,0) },
 { D3DXVECTOR3( -TILE_WIDTH/2, -TILE_HEIGHT/2, 0 ), D3DXVECTOR2(0,1)  },
 { D3DXVECTOR3( TILE_WIDTH/2, -TILE_HEIGHT/2, 0 ),   D3DXVECTOR2(1,1)  },
};
if(FAILED(m_pd3dDevice->CreateVertexBuffer(sizeof(rtVertices), 0,
 TexVertex::FVF, D3DPOOL_DEFAULT, &m_pRTVB, NULL)))
{
 return E_FAIL;
}

VOID* pRTVertices;
if(FAILED(m_pRTVB->Lock(0, sizeof(rtVertices), (void **)&pRTVertices, 0)))
 return E_FAIL;
memcpy(pRTVertices, rtVertices, sizeof(rtVertices));
m_pRTVB->Unlock();
만들어진 사각형이 타일의 크기와 정확히 일치해야합니다. 일치하지 않으면 약간의 왜곡이 생기겠죠? 하지만 타협을 줄수도 있겠습니다. 그리고 rtVertices는 static으로 함수내부에 해도 무방하겠죠?

전용 뷰행렬과 프로젝션 행렬이 눈에 띄실텐데, 이 부분을 제대로 해주셔야 합니다.
 // 렌더타겟용 매트릭스
 D3DXMatrixIdentity( &m_matRTView );
 D3DXVECTOR3 position( 0.0f, 0.0f, 0.0f );
 D3DXVECTOR3 target( 0.0f, 0.0f, 1.0f );
 D3DXVECTOR3 up( 0.0f, 1.0f, 0.0f );
 D3DXMatrixLookAtLH( &m_matRTView, &position, &target, &up );
 // 직교투영행렬로 만든다.
 D3DXMatrixOrthoLH( &m_matRTProj, TILE_WIDTH, TILE_HEIGHT, 0, 300 );

카메라를 0,0,0위치시키고 그저 +z방향을 바라보게 합니다. 또한 투영행렬은 직교투영으로 만들어서 왜곡이 일어나지 않게끔 해주어야 합니다. 이렇게 설정하고, 렌더링되는 정점은 Z=0인 부분에서 X, Y값이 각각
 -TILE_WIDTH/2~TILE_WIDTH/2 에 -TILE_HEIGHT/2~TILE_HEIGHT/2걸쳐서 렌더링되게 됩니다. (즉, 화면에 꽉찬단 이야기) 그러나 이것은 테스트 소스라 그대로 하시면 안되시구요. 실제 지형에선 아래를 참고해보세요.
일단 위의 버텍스버퍼설정 부분에서도 x,y가 아니구요 x,z쪽으로 타일을 대신할 사각형 정점정보를 두셔야합니다. 그리고 카메라는 원점에서 Y축만 높이값의 최대위치로 올려줍니다. 물론 밑을 향해바라보고 있어야 하구요. 직교행렬또한 원평면의 변화를 보실 수 있으실 겁니다.

 // 렌더타겟용 매트릭스
 D3DXMatrixIdentity( &m_matViewForBlend );
 // 최대높이로 카메라를 세팅해준다.
 D3DXVECTOR3 position( 0, MAX_HEIGHT_VALUE, 0 );
 D3DXVECTOR3 target( 0, 0.0f, 0 );
 D3DXVECTOR3 up( 0.0f, 0.0f, 1.0f );
 D3DXMatrixLookAtLH( &m_matRTView, &position, &target, &up );

D3DXMatrixOrthoLH( &m_matRTProj, TILE_WIDTH, TILE_HEIGHT, 0, MAX_HEIGHT_VALUE );

다시 돌아가서...
왜 타일의 크기와 딱 맞게 해주어야할까요? 다들 아시겠지만, 텍스쳐는 정점과 정점사이는 텍스쳐의 선형변환으로 이루어지게됩니다.(물론 다는 아닙니다.) 따라서 타일보다 작게 만들어버리면 그만큼 화질이 안좋아지게됩니다. 메모리 부분을 걱정하신다면 적당한 크기로 하셔도 무방합니다.

아랜 렌더링 부분입니다.

// 이전 렌더링 타겟을 저장한다.
m_pd3dDevice->GetRenderTarget( 0, &pOldBackBuffer );
m_pd3dDevice->GetDepthStencilSurface( &pOldZBuffer );
m_pd3dDevice->GetViewport( &oldViewport );

// 렌더링 타겟을 변경한다.
m_pd3dDevice->SetRenderTarget( 0, m_pRTBackBuffer );
m_pd3dDevice->SetDepthStencilSurface(m_pRTDepthBuffer);
m_pd3dDevice->SetViewport( &m_RTViewport );

// 렌더링 타겟 전용 행렬로 세팅합니다.
// 카메라가 0,0,0에 위치하고 있고 0,0,1을 바라보고 있고요,
// TILE_WIDTH/TILE_HEIGHT의 크기로 직교투영중에 있습니다. 근평면은 0입니다.
// 또한 정점들은 Z축값이 0이며 X, Y값은 각각 -TILE_WIDTH/2~TILE_WIDTH/2 에
// -TILE_HEIGHT/2~TILE_HEIGHT/2걸쳐서 렌더링되게 됩니다.
// 따라서 화면을 꽉차는 텍스쳐가 렌더링됩니다.
// 테스트 소스에는 없지만, 타일들을 원점으로 옮겨질 수 있도록 월드행렬을 세팅해줘야합니다.
m_pd3dDevice->SetTransform( D3DTS_VIEW, &m_matRTView );
m_pd3dDevice->SetTransform( D3DTS_PROJECTION, &m_matRTProj );

// 반드시 해주셔야합니다. 렌더타겟으로 지정되어있는 텍스쳐와 그의 깊이버퍼를 클리어해줍니다.
// 물론 한사이클에 한번씩 클리어 해주셔야 합니다.
// 즉, 베이스 텍스쳐 - 알파텍스쳐 - 조명셰이더 순으로 하신다면
// 렌더타겟(텍스쳐)클리어 - 베이스 텍스쳐 - 알파텍스쳐 - 렌더타겟(프레임버퍼)클리어 - 조명셰이더
m_pd3dDevice->Clear(0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, 0x00000000, 1.0f, 0);

// 불러온 텍스쳐를 렌더타겟에 렌더링한다.
m_pd3dDevice->SetRenderState( D3DRS_CULLMODE, D3DCULL_NONE );
m_pd3dDevice->SetTexture( 0, m_pRTTexture );
m_pd3dDevice->SetFVF( TexVertex::FVF );
m_pd3dDevice->SetStreamSource( 0, m_pRTVB, 0, sizeof(TexVertex) );
m_pd3dDevice->DrawPrimitive( D3DPT_TRIANGLESTRIP, 0, 2 );

// 위 과정을 nPass로 렌더링해서 합성해주어야 합니다.
알파옵션켜고 렌더링..
또 렌더링...
이로써 최종합성된 텍스쳐 한장이 만들어집니다.
이걸 가지고 셰이더코드에 넣어주시면 되겠습니다.

다음은 합성된 텍스쳐를 임의의 사각폴리곤에 렌더링하는 부분입니다.
 // 렌더링 타겟 복구
m_pd3dDevice->SetRenderTarget( 0, pOldBackBuffer );
m_pd3dDevice->SetDepthStencilSurface( pOldZBuffer );
m_pd3dDevice->SetViewport( &oldViewport );
// 반드시 릴리즈 해줘야한다.
pOldBackBuffer->Release();
pOldZBuffer->Release();
// 3D세계의 행렬로 맞춥니다.
m_pd3dDevice->SetTransform( D3DTS_VIEW, &m_matView );
m_pd3dDevice->SetTransform( D3DTS_PROJECTION, &m_matProj );
m_pd3dDevice->Clear(0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, 0xffffffff, 1.0f, 0);

m_pd3dDevice->SetTexture( 0, m_pRTBackBufferTexture );
m_pd3dDevice->SetFVF( TexVertex::FVF );
m_pd3dDevice->SetStreamSource( 0, m_pVB, 0, sizeof(TexVertex) );
m_pd3dDevice->DrawPrimitive( D3DPT_TRIANGLELIST, 0, 2 );

다음은 위 코드의 실행화면을 캡쳐한 사진입니다.






결론
일단 블렌딩 텍스쳐를 미리 만들어놓기 때문에 상당한 속도상의 이득을 얻을 수 있습니다.
허나 큰 맵의 경우 메모리를 너무 많이 차지하기때문에 로딩하다가 결국 가상메모리까지 들어가게되는 일이 발생할수도 있습니다.(2기가까지 넘어가는거보고 강제종료해버렸음) 따라서 더블버퍼링으로 실시간으로(카메라의 프러스텀이 닿기 바로 직전의 타일들) 블렌딩하시던지 어떤 대책을 취하셔야합니다. 그게 아니라면 그냥 멀티텍스쳐로 하시는게 나으시겠죠. 

하지만 작은 맵의 경우 메모리를 덜 차지하기 때문에 충분히 고려해볼 대상입니다.
그림자처리등으로 멀티패스로 렌더링할때도 조금이나마 부담을 덜어줄 수도 있으니까요.

아니 많은 부담을 덜어줄 수 있습니다.^^



읽어주셔서 감사합니다.