市面上基本所有的3D遊戲都依賴一個普通的windows視窗,包含标題欄、邊框、最小化、最大化、關閉按鈕。視窗的大小決定了玩家可視的遊戲空間,整個視窗的像素都被遊戲内容填充滿,視窗背景不是透明的。渲染時,隻要建立一個主渲染緩沖區,将各元素渲染在上面,再顯示就可以了。
本文介紹一種方法,視窗的背景是透明的,視窗中隻渲染主要的遊戲元素,比如主角,而windows桌面就是舞台,你可以看到你的角色在桌面上奔跑,還可以用滑鼠與它互動,如圖:
采用類似技術的遊戲有“哈姆寶寶”、“寵物王國”等。該技術比較适合寵物養成類遊戲,比傳統2D寵物的渲染方式要複雜的多。
//華麗的分隔線
先介紹下渲染的基本步驟:
1. 将角色渲染到貼圖(RenderToTexture),此時貼圖在顯存中
2. 貼圖内容拷貝到System Memory Texture,此時貼圖在記憶體中。(對顯示卡帶寬要求較高,在老舊的顯示卡上,大部分的性能都消耗在這個步驟,CPU占用率90%以上)
3. 從System Memory Texture“拷貝”到windows視窗的GDI位圖上。這裡不是簡單的拷貝,需要對貼圖中每個像素進行特殊處理。
4. 更新視窗内容,就完成顯示了
需要注意的是,步驟1和2中的貼圖,大小、格式一緻,并且與所在視窗的客戶區大小一緻(客戶區大小不一定等同于視窗大小)。是以,這裡都是逐像素拷貝,不存在任何拉伸問題。
按照以上渲染步驟,是以,渲染前要做的準備工作如下:
1. 建立視窗
2. 建立與視窗關聯的GDI位圖
3. 建立System Memory Texture
4. 建立渲染用的Texture
//華麗的分隔線
【準備步驟1】
拿上圖中的例子來說,渲染該寵物的貼圖大小為256x256,那麼首先要建立一個同等大小的Windows視窗,不帶邊框:
m_hWnd = ::CreateWindowEx(WS_EX_TOPMOST | WS_EX_TOOLWINDOW | WS_EX_LAYERED, ClassName, Title, WS_POPUP,0,0,256,256,0,NULL,hInst, NULL);
注意到style參數為WS_POPUP,就是建立不帶邊框、标題欄等内容的視窗,這時,客戶區大小等同于視窗大小。
WS_EX_LAYERED也是至關重要的一個屬性,用于實作視窗透明化功能,後面會提到。
其它參數請查閱MSDN。
【準備步驟2】
然後是建立GDI位圖,代碼如下:
BITMAPV4HEADERbm4;
bm4.bV4Width = 256; //視窗寬度
bm4.bV4Height = -256; //視窗高度。為什麼是負數,請查閱BITMAP相關技術文檔
bm4.bV4BitCount = 32; //像素比特數,這裡必須為32,像素格式A8R8G8B8
bm4.bV4Size = sizeof(BITMAPV4HEADER);
bm4.bV4Planes = 1;
bm4.bV4V4Compression = BI_RGB;
m_hDC = ::CreateCompatibleDC( 0 );
m_hBp = ::CreateDIBSection(m_hDC, (BITMAPINFO *)&bm4, DIB_RGB_COLORS, &m_pBitmapBits, 0, 0 );
m_hOldObj = ::SelectObject(m_hDC, m_hBp);
退出時記得銷毀:
if ( m_hDC )
{
::SelectObject( m_hDC, m_hOldObj );
::DeleteDC(m_hDC);
m_hDC = NULL;
}
if ( m_hBp )
{
::DeleteObject( m_hBp );
m_hBp = NULL;
}
變量聲明如下:
HDC m_hDC;
HBITMAP m_hBp;
HGDIOBJ m_hOldObj;
void * m_pBitmapBits;
【準備步驟3】
建立System Memory Texture:
hr = dev9->CreateOffscreenPlainSurface( 256, 256, D3DFMT_A8R8G8B8, D3DPOOL_SYSTEMMEM, &m_pSysMemSurface, NULL ); //D3DFMT_X8R8G8B8也可以
銷毀代碼:
if ( m_pSysMemSurface )
{
m_pSysMemSurface->Release();
m_pSysMemSurface = NULL;
}
變量聲明如下:
LPDIRECT3DSURFACE9 m_pSysMemSurface;
【準備步驟4】
建立渲染貼圖的方法就不講了,各引擎不一樣。
//華麗的分隔線
準備工作完畢後,可以開始渲染。
【渲染步驟1】先将物體渲染到RenderTexture
【渲染步驟2】從RT拷貝到System Memory Texture
LPDIRECT3DSURFACE9 rt_surface= NULL;
HRESULT hr;
hr = rt_texture->GetSurfaceLevel(0, &rt_surface);
//【渲染步驟2核心語句】
hr = dev9->GetRenderTargetData( rt_surface, m_pSysMemSurface );
rt_surface->Release();
//【渲染步驟3】從System Memory Texture到GDI位圖
D3D9SurfaceBlt2DIBPerPixelAlpha( m_hWnd, m_pSysMemSurface, m_hDC, m_pBitmapBits, 0xff);
//華麗的分隔線
D3D9SurfaceBlt2DIBPerPixelAlpha實作如下:
bool D3D9SurfaceBlt2DIBPerPixelAlpha( HWND window, LPDIRECT3DSURFACE9 pSurface, HDC srcDC, void *pSrcDCBmp, BYTE wndAlpha ) //隻支援32bit的surface
{
RECT wr = {0};
D3DSURFACE_DESC d3dsurface_desc;
D3DLOCKED_RECT d3drt = {0};
if ( !::GetClientRect(window, &wr ) )
{
return false;
}
POINT dstPT = {wr.left, wr.top};
if (!::ClientToScreen(window, &dstPT))
{
return false;
}
HRESULT hr = pSurface->LockRect( &d3drt, NULL, 0);//D3DLOCK_NOSYSLOCK|D3DLOCK_NO_DIRTY_UPDATE|D3DLOCK_READONLY );
assert( SUCCEEDED(hr) && d3drt.pBits );
if ( NULL == d3drt.pBits ) return false;
pSurface->GetDesc(&d3dsurface_desc);
//【渲染步驟3核心語句】
BltSurface32ToDIB32_SelfMulAlpha(pSrcDCBmp, d3drt.pBits, d3dsurface_desc.Width, d3dsurface_desc.Height, d3drt.Pitch);
pSurface->UnlockRect();
SIZE dstSZ = {wr.right - wr.left, wr.bottom - wr.top};
BLENDFUNCTION bfc = { AC_SRC_OVER, 0, wndAlpha, AC_SRC_ALPHA };
POINT srcPT = {0, 0};
//【渲染步驟4】這句就是通知視窗更新顯示内容,視窗必須擁有屬性WS_EX_LAYERED
::UpdateLayeredWindow(window, 0, &dstPT, &dstSZ,
srcDC, &srcPT, 0, &bfc, ULW_ALPHA );
return true;
}
//華麗的分隔線
BltSurface32ToDIB32_SelfMulAlpha函數是用彙編實作的,有三個版本:MMX、SSE、SSE2,可以固定選擇一個,也可以根據目前cpu支援的指令集動态選擇一個。
這裡就貼一個SSE版本,該函數的具體實作細節,因為年代久遠,我也記不太清了,有問題歡迎咨詢讨論 [email protected]。
【渲染步驟3】的關鍵問題是GDI位圖需要“自乘alpha”,懶得碼字了,請參閱MSDN,
BLENDFUNCTION和UpdateLayeredWindow。
void BltSurface32ToDIB32_SelfMulAlphaSSE( void *pDst, void *pSrc, unsigned int width, unsigned int height, unsigned int src_pitch )
{
int src_pitch_sub_dst_pitch;//src pointer對齊到下一行scanline,需要跳過多少位元組
__asm
{
//取參數,判斷width和height是否有任一為0
mov eax, height //eax = heigh
mov ebx, width
mul ebx //width * height
test eax, eax //影響ZF
jz end_pixel
//常量指派
mov esi, pSrc
mov edi, pDst
pcmpeqd mm5, mm5 //mm5 = 0xffffffff_ffffffff
pxor mm7, mm7 //mm7 = 0x0
psrld mm5, 8 //mm5 = 0x00ffffff_00ffffff
//判斷pitch
mov edx, src_pitch
shl ebx, 2 //每個像素4個位元組, dst_pitch = width * 4
sub edx, ebx //src_pitch - dst_pitch
jnz diff_pitch
//same_pitch:
mov ecx, eax
mov edx, 1 //how many lines,eax和edx構成2層循環
and ecx, 1 //一行上剩下多少個不成對的象素,same_pitch時就是(width*height & 1),diff_pitch時就是(width & 1)
shr eax, 1 //一行上主循環多少次,same_pitch時就是(width*height >> 1)diff_pitchh時就是(width >> 1)
jmp test_pair_pixel
diff_pitch:
mov src_pitch_sub_dst_pitch, edx //src_pitch - dst_pitch
mov eax, width
mov edx, height //how many lines,eax和edx構成2層循環
mov ecx, eax
and ecx, 1 //一行上剩下多少個不成對的象素,same_pitch時就是(width*height & 1),diff_pitch時就是(width & 1)
shr eax, 1 //一行上主循環多少次,same_pitch時就是(width*height >> 1)diff_pitchh時就是(width >> 1)
mov ebx, eax //main loop count on every scanline
jmp test_pair_pixel
loop_line:
mov eax, ebx
loop_pair_pixel:
movq mm0, [esi] //mm0 = 0xaarrggbb_AARRGGBB
movq mm4, mm5 //mm4 = mm5 = 0x00ffffff_00ffffff
movq mm1, mm0 //mm1 = mm0 = 0xaarrggbb_AARRGGBB
pandn mm4, mm0 //儲存alpha, mm4 = 0xaa000000_AA000000
punpcklbw mm0, mm7 //mm0 = 0x00AA_00RR_00GG_00BB
punpckhbw mm1, mm7 //mm1 = 0x00aa_00rr_00gg_00bb
pshufw mm2, mm0, 0xff //mm2 = 0x00AA_00AA_00AA_00AA
pshufw mm3, mm1, 0xff //mm3 = 0x00aa_00aa_00aa_00aa
pmullw mm0, mm2 //自乘alpha,字組相乘,取低16位
pmullw mm1, mm3
psrlw mm0, 8 //除以256
psrlw mm1, 8
packuswb mm0, mm0 //合并單個象素
packuswb mm1, mm1
punpckldq mm0, mm1 //将2個象素合并
pand mm0, mm5 //恢複原始alpha
por mm0, mm4
//put_pixel:
MOVNTQ [edi], mm0
add esi, 8
add edi, 8
dec eax
test_pair_pixel:
jnz loop_pair_pixel
//rest_line_pixel:
jecxz next_line //scanline_rest_pixel不是0就是1
movq mm0, [esi] //mm0 = 0xaarrggbb_AARRGGBB
movq mm4, mm5 //mm4 = mm5 = 0x00ffffff_00ffffff
movq mm1, mm0 //mm1 = mm0 = 0xaarrggbb_AARRGGBB
pandn mm4, mm0 //儲存alpha, mm4 = 0xaa000000_AA000000
punpcklbw mm0, mm7 //mm0 = 0x00AA_00RR_00GG_00BB
punpckhbw mm1, mm7 //mm1 = 0x00aa_00rr_00gg_00bb
pshufw mm2, mm0, 0xff //mm2 = 0x00AA_00AA_00AA_00AA
pshufw mm3, mm1, 0xff //mm3 = 0x00aa_00aa_00aa_00aa
pmullw mm0, mm2 //自乘alpha,字組相乘,取低16位
pmullw mm1, mm3
psrlw mm0, 8 //除以256
psrlw mm1, 8
packuswb mm0, mm0 //合并單個象素
packuswb mm1, mm1
punpckldq mm0, mm1 //将2個象素合并
pand mm0, mm5 //恢複原始alpha
por mm0, mm4
movd [edi], mm0
add esi, 4
add edi, 4
next_line:
add esi, src_pitch_sub_dst_pitch //設定指針到下一個src行
dec edx
jnz loop_line
emms
end_pixel:
}
}