멀티 패스 렌더링 이란 여러개의 경로를 가지는 렌더링 방법을 말한다. 보통 백 버퍼만으로 렌더링하는것을 싱글 패스 렌더링이라고 하며
멀티 패스 렌더링은 1개의 화면을 만들기 위해 여러개의 서피스에 렌더링하는 기술을 말한다
여기서는 멀티 패스 렌더링을 더욱 좁은뜻으로 다룰 것이다.
멀티 패스 렌더링에는 [ 한번에 여러개의 서피스에 렌더링 하는 기술] 이라는 의미도 있다.
예를들면 Tex1과 Tex2라는 서피스를 사용하여 백 버퍼에 렌더링 하는 경우
Tex1 생성, Tex2 생성, 백 버퍼로의 렌더링이라는 3번(3패스)의 렌더링이 필요하게 된다.
멀티 패스 렌더링을 사용하면 Tex1의 생성과 Tex2의 생성을 1패스에서 한번에 수행할수 있다.
렌더링 회수가 2번이 되기 때문에 퍼포먼스를 큰 폭으로 향상시킬 수 있다.
① 일단 렌더 타겟이 무엇인가..
렌더링에 대해서 조금 복습해보자. 우리들이 평소에 아무렇지도 않게 렌더링하고잇는 백버퍼...
왠지 [ 렌더링 = 백버퍼 ]라고 생각하기 쉽지만, 실은 여기에도 조금 생략되어있는 부분이 있다.
렌더링 디바이스는 디폴트 값으로 [ 렌더 타겟 0번]에 렌더링하도록 설정되어있으며,
백버퍼는 디폴트 값으로 렌더타겟 0번에 설정 되어있다.
즉 렌더링 디바이스와 백버퍼 사이에는 [ 렌더 타겟 ] 이라는 서피스 저장소가 있는 것이다.
이것을 확실히 알면 멀티 패스 렌더링도 쉽다
② IDirect3DDevice9::SetRenderTarget 함수
위의 그림처럼 백버퍼는 디폴트로 RT0에 설정되어 있긴 하지만 여기가 백버퍼의 고정석은 아니다.
IDirect3DDevice9::SetRenderTarget 함수를 사용하면 미리 만든 서피스를 RT0이나 RT1등으로 설정할 수 있다.:
IDirect3Device9::SetRenderTarget 함수 HRESULT SetRenderTarget( DWORD RenderTargetIndex, IDirect3DSurface9 *pRenderTarget ); |
RenderTargetIndex 에는 렌더 타겟의 번호를 설정한다
pRenderTarget에는 서피스를 넘긴다
주의 할 것은 SetRenderTarget 함수를 사용하면 이전에 설정되어있던 서피스의 포인터를 잃게 된다.
예를들어 RT0에 다른 서피스를 설정하면 백버퍼로 되돌아 올 수 없다
따라서 보통은 미리 설정된 서피스는 IDirect3DDevice9::GetRenderTarget으로 미리 저장해두고 그것을 바꾸는 형태로 사용한다
③ 지정한 렌더타겟에 렌더링 하기
RT0 이외의 서피스에서의 렌더링은 고정 기능 파이프라인에서는 불가능하며(아마도..)프로그래머블 셰이더에서 가능하다
셰이더(HLSL)을 사용하면 픽셀 셰이더에서 아래와같은 형태로 사용한다
float4 Test_PS( float2 texCoord : TEXCOORD0 ) : COLOR0 //<- 여기!!! { float outColor // 무언가 처리 return outColor; } |
COLOR0라고 표시되어 있는 부분에 주목 한다.
이 부분이 실은 렌더타겟 0번에 렌더링 하세요~라는 의미의 시멘틱스이다.
COLOR의 뒷부분의 숫자가 실은 쓰여질 렌더타겟의번호 라는 것이다. 즉 이 말은..
float4 Test_PS( float2 texCoord : TEXCOORD0 ) : COLOR1 //<- 여기!!! { float outColor // 무언가 처리 return outColor; } |
이렇게 하면 렌더타겟 1번에 쓰여진다는 것이 된다.
[ 이것은 싱글 패스 렌더링이잖아!!. 그럼 0번과 1번에 동시에 쓰려면 어떻게 해야하지?]
라고 생각할지도 모르지만 이 부분은 반환값을 구조체로 만들어 버리면 된다
struct OUTPUT_PS { float4 color0 : COLOR0; float4 color1 : COLOR1; };
OUTPUT_PS Test_PS( float2 texCoord : TEXCOORD0 ) { OUTPUT_PS outColor // 무언가 처리 return outColor; } |
④ MRT를 서포트 하는가에 주의
멀티 렌더 타겟은 비교적 최근엔 많이 쓰이지만 조금 옛날 비디오 카드면 서포트 하지 않는 경우도 있다
멀티 렌더 타겟으로써 가질 수 있는 서피스의 최대 수는 IDirect3DDevice9::GetDeviceCaps함수로 얻을수 있는 D3DCAPS9 구조체에서NumSimultaneousRTs의 체크한다
D3DCAPS9 Caps; pDev->GetDeviceCaps( &Caps ); DWORD MaxRT = Caps.NumSimultaneousRTs; |
멀티 렌더 타겟의 최대 수
이것이 1일 경우, 멀티 패스 렌더링은 할 수 없다. ELSA GLADIAC FX746Ultra DDR3(Geforce FX 5700 Ultra)에서는 1, 노트북 그래픽 카드인 (FMV-BIBLO MG70H 2004년 봄 모델 Intel(R) 82852/82855 GM/GME Graphics Controller)에서도 1이었다.
NVIDIA GeForce6600에서는 4였다.
대충 2005년 이후 나온 모델은 사용가능 할 것이라 판단된다
프로그램상에서 이것을 확인하는 것이 귀찮은 사람은 DirectX의 유틸리티 중「DirectX Caps Viewer(DXCapsViewer.exe)」를 사용하길 바란다. 이것은 비디오 카드의 능력을 열거해 주는 어플리케이션이다.
자신의 비디오카드 폴더 내에서 [D3D Device Types]→[HAL]→[Caps]안에 위 플래그가 있을 것이다
⑤ 멀티 패스 렌더링으로 무엇을 할 것인가?
멀티 패스 렌더링을 함으로 뭐가 가능한가? 이것은 딱 이거다 라고 할순 없지만 근래의 복잡하며 여러 장의 텍스처를 겹쳐서 사용하는 렌더링 표현에서는 필수 항목인 기술인 것은 확실하다.
특히 [ 동적으로 텍스처를 만드는 ] 이펙트에서 효과적이다
동적으로 만든다고 하면 포스트 이펙트 (렌더링한 그림에 뭉개기 효과나 세피아톤등으로 가공하는 후처리), 큐브 맵 생성, 거울 효과등등 여러가지 있을 것이다.
멀티 패스 렌더링에서의 또다른 장점은 버텍스 셰이더와의 균형이다.
버텍스 셰이더에서는 매우 복잡한 본 조작, 픽셀 셰이더에서의 라이트 처리를 위한 법선 설정,
버텍스 변환등 귀찮은 것을 여러가지 수행한다.
싱글 패스 렌더링의 경우 매번 이 작업을 반복해야 하는 반면 멀티 패스 렌더링에서는 대부분의 버텍스 셰이더 작업을 1번으로 끝낼 수 있다. 이것은 퍼포먼스 향상으로 연결 된다.
멀티 패스 렌더링에서 뭐가 가능한가는 셰이더 프로그램을 배워가면 자연히 알게 되겠지만 연습을 위해 RGB 칼라와 알파 정보를 2개의 서피스( 하나는 백 버퍼)에 동시에 렌더링해보자
⑥ 멀티 패스 렌더링 예 -1:색과 알파 값을 동시에 렌더링
이것은 DirectX 부록으로 있는 비행기 모델을 렌더링 한다.
이때 RGB 칼라와 알파 값을 분해 해어 별도의 텍스처로 한번 렌더링 한다.
그리고 이 결과를 교대로 화면에 렌더링 한다.
이것이 어디에 쓰이는가 보다는 멀티 패스 렌더링의 본질을 이해 하는데 집중해주길 바란다.)
○ 셰이더 프로그램
우선은 셰이더 프로그램을 만든다. 목표는 픽셀 셰이더에서 렌더링 정보를 RGB와 알파값으로 분해하는 것이다
float4x4 WVP; // WorldViewProjection Matrix texture Tex; // Texture sampler2D TexSampler = sampler_state { Texture = (Tex); };
struct OUTPUT_VS { float4 pos : POSITION; float2 texCoord : TEXCOORD0; };
struct OUTPUT_PS { float4 color : COLOR0; float4 alpha : COLOR1; };
OUTPUT_VS SimpleVS( float4 inPos : POSITION, float2 inTexCoord : TEXCOORD0 ) { OUTPUT_VS outVS = (OUTPUT_VS)0; outVS.pos = mul( inPos, WVP ); outVS.texCoord = inTexCoord; return outVS; }
OUTPUT_PS RGBAndAlphaMPR_PS( float2 texCoord : TEXCOORD0 ) { OUTPUT_PS PSout = (OUTPUT_PS)0; PSout.color = tex2D( TexSampler, texCoord ); PSout.alpha = PSout.color.a; PSout.alpha.a = 1.0f; return PSout; }
technique RGBAndAlphaMPR { pass p0 { // 렌더 스테이트 설정 AlphaBlendEnable = TRUE; SrcBlend = SRCALPHA; DestBlend = INVSRCALPHA; ColorOp[0] = SELECTARG1; ColorArg1[0] = TEXTURE; ColorArg2[0] = DIFFUSE; AlphaOp[0] = SELECTARG1; AlphaArg1[0] = TEXTURE; AlphaArg2[0] = DIFFUSE; ColorOp[1] = DISABLE; AlphaOp[1] = DISABLE;
// 셰이더 VertexShader = compile vs_2_0 SimpleVS(); PixelShader = compile ps_2_0 RGBAndAlphaMPR_PS(); } } |
셰이더 프로그램 (RGBAndAlphaMPR.fx)
월드-뷰-프로젝션 행렬로써 WVP를 선언한다.
다음으로 Tex는 모델의 입히는 텍스처이다
이것은 픽셀 셰이더에서 사용한다
sampler는 텍스처로부터 색을 취하는 형태이지만 이번엔 Tex로부터 얻을 수 있도록 설정하였다.
원래는 샘플링 방법등을 정의하지만 여기서는 간단하게 하기 위해 생략한다.
다음 OUTPUT_VS는 버텍스 셰이더의 반환값에 대한 구조체이다.
이번엔 변환후의 버텍스 단위와 텍스처 좌표(UV 좌표)가 필요하므로 위와 같이 설정한다.
그 다음에 있는 OUTPUT_PS가 여기에서의 핵심이다. 여기에는 2개의 COLOR가 설정되어있다.
OUTPUT_PS::alpha에 알파 정보가 저장되며, 이것이 렌더타겟 1번에 출력될 값이다.
SimpleVS 버텍스 셰이더는 딱히 무언가 하는 것은 아니다 입력된 로컬 버텍스를 WVP행렬을 사용해 스크린 좌표로 변환한다
UV좌표도 구조체에 복사해서 반환값을 전달 하고 있다
픽셀 셰이더에서는 넘어온 UV 좌표에 해당하는 텍셀(텍스처 좌표상의 픽셀)을 tex2D함수로 얻어온다.
이것은 픽셀 셰이더의 기본이다.
얻어온 색으로부터 알파값을 PSout.alpha에 대입하고 있다.
마지막으로 return에서 구조체를 반환한다.
이것으로 한번에 2개의 렌더 타겟에서 렌더링된다。
○ 테크닉
technique RGBAndAlphaMPR { pass p0 { // 렌더 스테이트 설정 AlphaBlendEnable = TRUE; SrcBlend = SRCALPHA; DestBlend = INVSRCALPHA; ColorOp[0] = SELECTARG1; ColorArg1[0] = TEXTURE; ColorArg2[0] = DIFFUSE; AlphaOp[0] = SELECTARG1; AlphaArg1[0] = TEXTURE; AlphaArg2[0] = DIFFUSE; ColorOp[1] = DISABLE; AlphaOp[1] = DISABLE;
// 셰이더 VertexShader = compile vs_2_0 SimpleVS(); PixelShader = compile ps_2_0 RGBAndAlphaMPR_PS(); } } |
테크닉 (RGBAndAlphaMPR.fx)
알파 블렌드를 유효하게 해야 하므로 AlphaBlendEnable을 TRUE로 이하 알파 블렌드를 구현하는 스탠다드한 설정을한다
○ 프로그램
// 멀티 렌더 타겟 테스트 #pragma comment(lib, "dxguid.lib") #pragma comment(lib, "d3d9.lib") #pragma comment(lib, "d3dx9.lib")
#include <windows.h> #include <tchar.h> #include <d3d9.h> #include <d3dx9.h> #include <math.h>
TCHAR gName[100] = _T("멀티 렌더 타겟");
#define SAFERELEASE(x) if(x){x->Release();} #define FULLRELEASE \ SAFERELEASE(pAlphaSurf); \ SAFERELEASE(pBackBuffer); \ SAFERELEASE(pEffect); \ SAFERELEASE(pErrorBuffer); \ SAFERELEASE(pAirPlaneTex); \ SAFERELEASE(pAirPlane); \ SAFERELEASE(pAlphaTex); \ SAFERELEASE(g_pD3DDev); \ SAFERELEASE(g_pD3D);
#define FAILEDCHECK(x) \ if(FAILED(x)) { \ FULLRELEASE; \ return 0; \ }
LRESULT CALLBACK WndProc(HWND hWnd, UINT mes, WPARAM wParam, LPARAM lParam){ if(mes == WM_DESTROY || mes == WM_CLOSE ) {PostQuitMessage(0); return 0;} return DefWindowProc(hWnd, mes, wParam, lParam); }
int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow) { // 어플리케이션 초기화 MSG msg; HWND hWnd; WNDCLASSEX wcex ={sizeof(WNDCLASSEX), CS_HREDRAW | CS_VREDRAW, WndProc, 0, 0, hInstance, NULL, NULL, (HBRUSH)(COLOR_WINDOW+1), NULL, (TCHAR*)gName, NULL}; if(!RegisterClassEx(&wcex)) return 0;
if(!(hWnd = CreateWindow(gName, gName, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, 640, 480, NULL, NULL, hInstance, NULL))) return 0;
// Direct3D 초기화 LPDIRECT3D9 g_pD3D; LPDIRECT3DDEVICE9 g_pD3DDev; if( !(g_pD3D = Direct3DCreate9( D3D_SDK_VERSION )) ) return 0;
D3DPRESENT_PARAMETERS d3dpp = {640,480,D3DFMT_UNKNOWN,0,D3DMULTISAMPLE_NONE,0, D3DSWAPEFFECT_DISCARD,NULL,TRUE,TRUE,D3DFMT_D24S8,0,D3DPRESENT_RATE_DEFAULT,D3DPRESENT_INTERVAL_DEFAULT};
if( FAILED( g_pD3D->CreateDevice( D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWnd, D3DCREATE_HARDWARE_VERTEXPROCESSING, &d3dpp, &g_pD3DDev ) ) ) if( FAILED( g_pD3D->CreateDevice( D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWnd, D3DCREATE_SOFTWARE_VERTEXPROCESSING, &d3dpp, &g_pD3DDev ) ) ) { g_pD3D->Release(); return 0; }
D3DCAPS9 Caps; g_pD3DDev->GetDeviceCaps( &Caps ); DWORD RT = Caps.NumSimultaneousRTs; if ( RT <= 1 ) { MessageBox( hWnd, _T("멀티 렌더 타겟을 지원하지 않습니다 종료합니다"), _T("그 외 에러"),0); g_pD3DDev->Release(); g_pD3D->Release(); return 0; }
// 변수 정의 ID3DXMesh *pAirPlane = 0; IDirect3DTexture9 *pAirPlaneTex = 0; IDirect3DTexture9 *pAlphaTex = 0; ID3DXEffect *pEffect = 0; ID3DXBuffer *pErrorBuffer = 0; IDirect3DSurface9 *pAlphaSurf = 0; IDirect3DSurface9 *pBackBuffer = 0;
// 비행기 오브젝트 생성 DWORD NumMaterials; FAILEDCHECK( D3DXLoadMeshFromX( _T("airplane 2.x"), D3DXMESH_MANAGED, g_pD3DDev, NULL, NULL, NULL, &NumMaterials, &pAirPlane) ); FAILEDCHECK( D3DXCreateTextureFromFile( g_pD3DDev, _T("AirPlaneTex.png"), &pAirPlaneTex ) );
// 이펙트 생성 FAILEDCHECK( D3DXCreateEffectFromFile( g_pD3DDev, _T("RGBAndAlphaMPR.fx"), 0, 0, 0, 0, &pEffect, &pErrorBuffer) );
// 알파용 텍스처 생성 FAILEDCHECK( g_pD3DDev->CreateTexture( 640, 480, 1, D3DUSAGE_RENDERTARGET, D3DFMT_A8R8G8B8, D3DPOOL_DEFAULT, &pAlphaTex, 0 ) );
// 렌더 타겟 세팅 pAlphaTex->GetSurfaceLevel( 0, &pAlphaSurf ); g_pD3DDev->SetRenderTarget( 1, pAlphaSurf ); g_pD3DDev->GetRenderTarget( 0, &pBackBuffer );
// 각 행렬 생성 D3DXMATRIX WorldMat, ViewMat, ProjMat, WVP; D3DXMatrixIdentity( &WorldMat ); D3DXMatrixPerspectiveFovLH( &ProjMat, D3DXToRadian(45), 640.0f/480.0f, 0.1f, 100.0f);
ShowWindow(hWnd, nCmdShow);
// 메시지 루프 int count = 0; double angle = 0.0; do{ Sleep(1); if( PeekMessage(&msg, NULL, 0, 0, PM_REMOVE) ){ DispatchMessage(&msg);}
g_pD3DDev->Clear( 0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, D3DCOLOR_XRGB(40,40,80), 1.0f, 0 ); g_pD3DDev->BeginScene();
// 뷰 회전 angle += 0.01f; D3DXMatrixLookAtLH( &ViewMat, &D3DXVECTOR3((float)(10.0f*cos(angle)),5,(float)(10.0f*sin(angle))), &D3DXVECTOR3(0,0,0), &D3DXVECTOR3(0,1,0) ); WVP = WorldMat * ViewMat * ProjMat;
// 렌더 타겟 교환 if ( count++ % 60 == 0 ) { IDirect3DSurface9 *pTmp = pAlphaSurf; pAlphaSurf = pBackBuffer; pBackBuffer = pTmp; g_pD3DDev->SetRenderTarget( 1, NULL ); // 이부분 g_pD3DDev->SetRenderTarget( 0, pBackBuffer ); g_pD3DDev->SetRenderTarget( 1, pAlphaSurf ); }
// 렌더 디바이스에 이펙트 세팅 pEffect->SetMatrix("WVP", &WVP ); pEffect->SetTexture("Tex", pAirPlaneTex ); pEffect->SetTechnique( "RGBAndAlphaMPR" ); unsigned int numPass; pEffect->Begin( &numPass, 0 ); pEffect->BeginPass( 0 ); unsigned int i; for( i = 0; i < NumMaterials; i++ ) pAirPlane->DrawSubset( i ); pEffect->EndPass();
g_pD3DDev->EndScene(); g_pD3DDev->Present( NULL, NULL, NULL, NULL ); }while(msg.message != WM_QUIT);
FULLRELEASE;
return 0; } |
메인 프로그램(main.cpp)
오왕~ 이렇게 하면 픽셀 단위로 픽킹이 가능하겠네요.
근데 동일한 화면 크기를 사용하면 고해상도시 메모리 사용량도 높을거 같은데...
픽킹용 렌더 타겟은 저해상도로 해서 찾아내도 되지 않으까요?
정확한 픽셀 단위까지 체크할거 아니면 상관 없을거 같은데...
적당히 저해상도로 해도 되죠.. 실제로 고해상도로 해도 MSAA쓰면 경계 부분은 아마 정확한 해쉬값이 안나올거에요....
DX9 이하에서는 픽셀값을 읽어올 때, GetRenderTargetData를 사용해야 하는 것으로 알고 있는데, 이 함수 자체가 하드웨어특성을 많이 타는 문제있는 함수라, 경우에 따라서는 picking ray를 이용한 검출 보다 느린 것으로 알고 있습니다. 혹시 다른 방법으로 픽셀을 읽는 방법이 있는지요?
어차피 개발자용 컴퓨터에서 사용할 함수니까(맵 에디터를 공개 안한단 조건하에..) 별 상관없지 않나요?
한마디로 다른 방법은 모릅니다. ^_^ surface를 통채로 systemmem에 있는 렌더타겟에 복사한 뒤 거기서 찾아보면 어떨까요.. -_-;
기억이 확실하지 않은데 제가 기억하는게 맞다면, 아마 이 방법이 고전적인 OpenGL 에서 기본 제공하는 피킹의 내부 메카니즘일겁니다.
http://content.gpwiki.org/index.php/OpenGL:Tutorials:Picking
이쪽에서는 자체 API를 이용하는거니 물론 화면 데이터를 다 가져올 필요가 없지요.
일단 화면 메모리 락걸고 메모리 카피하느라 생기는 성능저하를 생각하면, 아무래도 직접 OpenGL API를 (다룰 수 있다면) 다루는게 훨씬 빠를르겠네요. (OpenGL 코드는 매우 간단하죠.)
DX 도 비슷한게 있을꺼 같은데..... DX는 잘 모르겠고.... @.@
DX자체에서 지원하는 방법은 없는것 같구요... 뭐 전 여러번 써본 방법인데 툴에서 쓰기에 속도저하는 크게 걱정할 부분이 아니었습니다.. 정 속도저하가 되면 그냥 더블 버퍼링 걸면 되겠죠.. -_-; (1프레임 딜레이 쯤이야!....)
쉬운데.....? -_-a (오호?) 첨에 공 그림들이 뭔소리인지 이해가 안가서 글만 정독하니까 그때야 그림이 이해가 가네요. 그림에 민감한것도 참 이럴땐 단점 ...
저도 나중에 툴 만들고 막 이렇게 되면 써야겠어요. 툴에서야 뭐 퍼포먼스따위... 하드웨어빨로 밀어붙이면 되니까..
제가 아트에 약합니다.. 흑흑 ..ㅜ_ㅜ