使用規(guī)范GDI完成游戲品質(zhì)的動(dòng)畫(huà)系統(tǒng)
發(fā)表時(shí)間:2024-06-19 來(lái)源:明輝站整理相關(guān)軟件相關(guān)文章人氣:
[摘要]燕良 2002年1月http://www.diamondgarden.net/ 前言 2GDI基礎(chǔ) 3繪制一個(gè)位圖(BITMAP)對(duì)象 3常用像素格式 4WINDOWS下的基本動(dòng)畫(huà)系統(tǒng) 4動(dòng)畫(huà)驅(qū)動(dòng)方式 4播放動(dòng)畫(huà) 5消除閃爍 6透明色(COLOR KEY)處理 7ALPHA混合 9讀取JPEG,G...
燕良 2002年1月
http://www.diamondgarden.net/
前言 2
GDI基礎(chǔ) 3
繪制一個(gè)位圖(BITMAP)對(duì)象 3
常用像素格式 4
WINDOWS下的基本動(dòng)畫(huà)系統(tǒng) 4
動(dòng)畫(huà)驅(qū)動(dòng)方式 4
播放動(dòng)畫(huà) 5
消除閃爍 6
透明色(COLOR KEY)處理 7
ALPHA混合 9
讀取JPEG,GIF文件 10
子窗口管理 12
進(jìn)階技巧--使用DIB 14
像素操作 14
RLE壓縮 15
參考 15
華山論鍵 15
其它類(lèi)庫(kù) 16
前言
說(shuō)到實(shí)現(xiàn)游戲品質(zhì)的動(dòng)畫(huà),很多人會(huì)立刻想到DirectX,沒(méi)錯(cuò)DirectDraw很強(qiáng)大,但是并不是必須用DirectDraw才行。動(dòng)畫(huà)后面的理論和技巧都是一樣的,這和末端使用什么API沒(méi)有太大關(guān)系(如果那API不是太~~慢的話)。就筆者實(shí)現(xiàn)的NewImage Lib的測(cè)試結(jié)果,內(nèi)部所有像素?cái)?shù)據(jù)的存儲(chǔ)和運(yùn)算都純軟件實(shí)現(xiàn),最后一步輸出到屏幕使用GDI的性能比DirectDraw低不到10%,在Window9X系統(tǒng)上要低20%左右,這對(duì)很多軟件來(lái)說(shuō)是絕對(duì)可以接受的。
現(xiàn)在應(yīng)用程序界面越做越華麗,除了支持SKIN外,很多人都想在程序中加入一些例如sprite動(dòng)畫(huà)這種原本用在游戲上的技術(shù),因?yàn)檫@原因引入DirectX API,顯然是不值得的(況且DX版本升級(jí)頻繁,DX8中已經(jīng)用DirectGraphic取代了DirectDraw)。本文將以筆者使用標(biāo)準(zhǔn)GDI函數(shù)實(shí)現(xiàn)的商業(yè)游戲?yàn)槔,帶你進(jìn)入高品質(zhì)2D動(dòng)畫(huà)編程領(lǐng)域,并且保證其設(shè)備無(wú)關(guān)性。
本文假設(shè)讀者有C/C++語(yǔ)言知識(shí),Windows編程基礎(chǔ),GDI基本概念。下面我將主要講述我在過(guò)去工作中積累的經(jīng)驗(yàn)和一些技巧,但是將不講解以上基本概念。讀者最好有MFC基礎(chǔ),本文給出的代碼將主要使用MFC,但是其中的道理卻不限于MFC。
GDI基礎(chǔ)
繪制一個(gè)位圖(Bitmap)對(duì)象
GDI的所有操作都是在DC(device context)上進(jìn)行的,所以首先你應(yīng)該有DC的概念,如果你對(duì)DC還不了解,現(xiàn)在就去翻一翻Windows編程的書(shū)吧。
首先我們要Load一個(gè)Bitmap對(duì)象,使用Win32 API可以寫(xiě)成這樣:
file://從資源Load一個(gè)位圖,如果從文件load的話,可以使用::LoadImage()
HBITMAP hbmp=::LoadBitmap(hInstance,MAKEINTRESOURCE(IDB_MYBMP));
如果使用MFC可以這樣寫(xiě):
CBitmap bmp;
Bmp.LoadBitmap(IDB_MYBMP);
想把這個(gè)位圖對(duì)象繪制到窗口上就要先得到窗口的DC,然后對(duì)這個(gè)DC操作。請(qǐng)留意創(chuàng)建MemoryDC的代碼,后面會(huì)用到。
Win32 API的版本:
file://假設(shè)位圖大小為100*100像素
file://假設(shè)hwnd是要繪制的窗口的HANDLE
HDC hwnddc=::GetDC(hwnd);
HDC memdc=::CreateCompatibleDC(hwnddc);
HBITMAP oldbmp=::SelectObject(memdc,hbmp);
::BitBlt(hwnddc,0,0,100,100,memdc,0,0,SRCCOPY);
if(oldbmp)
::SelectObject(memdc,oldbmp);
DeleteDC(memdc);
::ReleaseDC(hwnd,hwnddc);
MFC版本:
file://假設(shè)是在一個(gè)CWnd派生類(lèi)的成員函數(shù)中
CClientDC dc(this);
CDC memdc;
memdc.CreateCompatibleDC(&dc);
CBitmap *oldbmp=memdc.SelectObject(&bmp);
dc.BitBlt(0,0,100,100,&memdc,0,0,SRCCOPY);
if(oldbmp)
memdc.SelectObject(oldbmp);
也可以這樣:
CClientDC dc(this);
dc.DrawState(CPoint(0,0),CSize(100,100),&bmp,DST_BITMAP);
基本的代碼就是這樣,當(dāng)然有更多的API可以用,這就要看你自己的了。J
常用像素格式
要進(jìn)行圖像編程的化對(duì)像素格式不了解似乎說(shuō)不過(guò)去。我想應(yīng)該有較多的人并不太了解,所以這里簡(jiǎn)要的介紹一下。
1. 8bit
也叫做256色模式。每個(gè)像素占一個(gè)字節(jié), 使用調(diào)色板。調(diào)色板實(shí)際上是一個(gè)顏色表,簡(jiǎn)單的講就是,我們有256個(gè)油漆桶(因?yàn)橄袼氐娜≈捣秶?到255),每個(gè)油漆桶里面漆的顏色都由紅,綠,藍(lán)(RGB)三中基本的油漆按不同比例配置而成。所以我們指定一個(gè)像素的顏色的時(shí)候只需要指定它用的第幾號(hào)桶就好了。
這種模式造就了DOS時(shí)代的神奇模式—13H(320*200*256色),因?yàn)?20*200*1Byte正好是16bit指針尋址能力的范圍。這種模式有2的18次方種顏色(通過(guò)改變調(diào)色板實(shí)現(xiàn)),可以同時(shí)顯示256中顏色。這模式剛剛推出的時(shí)候,有人驚呼這是人類(lèi)智慧的結(jié)晶呢!也是這種模式造就了1992年WestWood的<<卡蘭蒂亞傳奇>>和1995年大宇資訊的<<仙劍奇?zhèn)b傳>>這樣的經(jīng)典游戲。
在Windows下硬件調(diào)色板應(yīng)該極少用到,但是你可以用軟件調(diào)色板來(lái)壓縮你的動(dòng)畫(huà),這也是在2D游戲中常用的技巧。
2. 16bit
這也是筆者最喜歡的模式。它不使用調(diào)色板。每個(gè)像素占兩個(gè)字節(jié),存儲(chǔ)RGB值。我覺(jué)得這種像素格式的效果(同時(shí)顯示顏色數(shù))和存儲(chǔ)量(也影響速度)取得了比較好的統(tǒng)一。但是如果你是寫(xiě)應(yīng)用程序的話,我勸你不要用它。因?yàn)樗腞GB值都不是整個(gè)BYTE,例如565模式(16bit的一種模式),它的RGB所占用的bit就是這樣的:
RRRR RGGG GGGB BBBB
3. 24bit
每個(gè)像素有三個(gè)BYTE,分別存儲(chǔ)RGB值,這對(duì)你來(lái)說(shuō)是不是很方便?是不是太好了?可惜對(duì)我們可憐的計(jì)算機(jī)卻不是,因?yàn)镃PU訪問(wèn)奇數(shù)的地址會(huì)很費(fèi)勁,而且在硬件工藝上也有很多困難(具體我也不太清楚,請(qǐng)做過(guò)硬件的高手指點(diǎn)),所以你會(huì)發(fā)現(xiàn)你的顯卡不支持這種模式,但是你可以在自己的軟件中使用。
4. 32bit
每個(gè)像素4個(gè)BYTE,分別存儲(chǔ)RGBA,A值就是Alpha,也就是透明度,可以用像素混合算法實(shí)現(xiàn)多種效果,后面你就會(huì)看到。
Windows下的基本動(dòng)畫(huà)系統(tǒng)
動(dòng)畫(huà)驅(qū)動(dòng)方式
先略說(shuō)一下動(dòng)畫(huà)的基本原理,程序播放動(dòng)畫(huà)一般過(guò)程都是: 繪制—擦除—繪制,這樣的重復(fù)過(guò)程,只要你重復(fù)的夠快,至少每秒16次(被稱(chēng)作16FPS,F(xiàn)rame per Second),我們可憐的眼睛就分辨不出單幀的圖像了,看上去就是動(dòng)畫(huà)了。
在Windows環(huán)境下要驅(qū)動(dòng)這樣重復(fù)不停的操作有兩種方法:
1. 設(shè)置Timer
這很簡(jiǎn)單,只要設(shè)置一個(gè)足夠短的Timer,然后響應(yīng)WM_TIME(對(duì)應(yīng)MFC中的OnTimer函數(shù))就可以滿足絕大部分應(yīng)用程序的需要。缺點(diǎn)是不夠精確,而且Win2000和Win9x系統(tǒng)的精確性又有較大差異。
2. 在消息循環(huán)中執(zhí)行動(dòng)畫(huà)操作
這是在游戲中常用的方法,一般都會(huì)把WinMain中的消息循環(huán)寫(xiě)成這樣:
while( TRUE )
{
// Look for messages, if none are found then
// update the state and display it
if( PeekMessage( &msg, NULL, 0, 0, PM_NOREMOVE ) )
{
if( 0 == GetMessage(&msg, NULL, 0, 0 ) )
{
// WM_QUIT was posted, so exit
return (int)msg.wParam;
}
TranslateMessage( &msg );
DispatchMessage( &msg );
}
else
{
if( g_bActive )//在主窗口不激活時(shí)不更新,以節(jié)省資源
{
file://執(zhí)行動(dòng)畫(huà)更新操作
}
// Make sure we go to sleep if we have nothing else to do
else WaitMessage();
}
}
如果你使用MFC,則需要重載CWinAPP的Run虛函數(shù),把上述消息循環(huán)替換進(jìn)去。
播放動(dòng)畫(huà)
現(xiàn)在我們有了一個(gè)適當(dāng)?shù)臅r(shí)機(jī)執(zhí)行更新操作了,現(xiàn)在就讓我們?cè)囋噭?dòng)畫(huà)吧。下面的代碼將不再提供Win32的版本。
為了敘述方便,我需要一個(gè)播放動(dòng)畫(huà)的窗口,它必須是一個(gè)CWnd的派生類(lèi),假設(shè)這個(gè)類(lèi)叫做CMyView,我們將在這個(gè)窗口中繪制動(dòng)畫(huà)。首先我們?yōu)檫@個(gè)類(lèi)添加一個(gè)成員函數(shù)”void CMyView::RenderView()”,你可以使用上面提到的方法調(diào)用這個(gè)函數(shù)。
現(xiàn)在準(zhǔn)備工作都做好了,我們的動(dòng)畫(huà)該怎么存儲(chǔ)呢?別提動(dòng)畫(huà)GIF89a格式(如果你覺(jué)得只有GIF才有動(dòng)畫(huà)的話,那我勸你去做美術(shù)好了,別干程序了),如果你只想要個(gè)簡(jiǎn)單的動(dòng)畫(huà)播放當(dāng)然可以,但是如果你想要做復(fù)雜點(diǎn)的,交互式動(dòng)畫(huà),我勸你還是別用那東西。假設(shè)我們有一個(gè)4幀的動(dòng)畫(huà),怎么存儲(chǔ)它呢?我首先想到的就是存4個(gè)BMP文件,然后讀入到一個(gè)CBitmap對(duì)象數(shù)組中,但是尊敬的大師Scott Meyers警告我們不要使用多態(tài)數(shù)組,因?yàn)榫幾g器在某些情況下不能準(zhǔn)確計(jì)算數(shù)組中對(duì)象的大小,所以下標(biāo)運(yùn)算符會(huì)產(chǎn)生可怕的效果。然后我就想到了用CBitmap指針數(shù)組,這到是不錯(cuò),不過(guò)管理起來(lái)稍嫌麻煩,F(xiàn)在看看我最終的解決方法吧。把一個(gè)幀序列安順序拼接成一個(gè)文件,象這樣:
然后用它創(chuàng)建一個(gè)CImageList對(duì)象,讓我們仔細(xì)看一下創(chuàng)建的方法,使用
BOOL CImageList::Create( int cx, int cy, UINT nFlags, int nInitial, int nGrow );
函數(shù),前面兩個(gè)參數(shù)用來(lái)指定我們一幀動(dòng)畫(huà)的尺寸。這樣就創(chuàng)建了一個(gè)空的ImageList,這樣做的好處是可擴(kuò)展行比較強(qiáng)。下面我們需要把那個(gè)幀序列文件Load到一個(gè)CBitmap對(duì)象中,你可以存成JPG或者GIF文件來(lái)節(jié)省容量(后面將提到讀取這些文件的簡(jiǎn)單方法,并且附一個(gè)實(shí)用類(lèi))。當(dāng)我們有了一個(gè)合適的CBitmap對(duì)象后,可以把他添加到我們的ImageList中,使用:
BOOL CImageList::int Add( CBitmap* pbmImage, COLORREF crMask );
一個(gè)實(shí)例:
const int SPRIRT_WIDTH=32;
const int SPIRIT_HEIGHT=32;
….
m_myimglist.Create(SPIRIT_WIDTH,SPIRIT_HIGHT,ILC_COLOR24 ILC_MASK,1,1);
if(bmp.Load(“myani.bmp”))
m_myimglist.Add(&bmp,RGB(152,152,152));
好了,現(xiàn)在我們已經(jīng)準(zhǔn)備好了這些數(shù)據(jù),讓我們來(lái)實(shí)作渲染函數(shù)吧,下面這端代碼可以循環(huán)播放上面的4幀動(dòng)畫(huà),并且支持透明色(如果你不知道這個(gè)名字,稍后有講解)哦!
void CMyView::RenderView()
{
CclientDC dc(this);
Static int curframe=0;
m_myimglist.Draw(&dc,curframe,Cpoint(0,0),ILD_TRANSPARENT);
curframe++;
If(curframe > m_myimglist.GetImageCount())
Curframe=0;
}
上面這個(gè)代碼沒(méi)有寫(xiě)擦除的操作,因?yàn)檫@根據(jù)具體需要有較大不同。如果你只有一個(gè)精靈動(dòng)畫(huà)的話,你可以用一個(gè)Bitmap對(duì)象保存精靈所占矩形區(qū)域的圖像。你也可能需要有一個(gè)大的背景圖每幀都要更新(這里我不討論象dirty rect這樣的優(yōu)化方法),所以你只要每次都畫(huà)背景,然后畫(huà)精靈就好了。
怎么樣?你已經(jīng)實(shí)現(xiàn)了基本的動(dòng)畫(huà)系統(tǒng),就是這么簡(jiǎn)單。
消除閃爍
如果你真正實(shí)現(xiàn)上面的代碼的話,你會(huì)發(fā)現(xiàn)畫(huà)面一閃一閃的,十分的不爽。L 很多人都會(huì)怪到GDI頭上,他們又會(huì)罵MS,說(shuō)GDI太慢了。其實(shí)非也(不是指MS不該罵,呵呵),任何直接寫(xiě)屏幕的操作都會(huì)產(chǎn)生閃爍,在DOS下直接寫(xiě)顯存或者用DirectDraw API直接寫(xiě)Primary Surface都會(huì)閃爍,因?yàn)槟忝總(gè)更新顯示的操作都會(huì)被用戶馬上看到(因?yàn)榇怪被貟叩脑颍?或許會(huì)有延遲)。
消除閃爍最簡(jiǎn)單也是最經(jīng)典的方法就是雙緩沖(Double buffer)。所謂的雙緩沖其實(shí)道理非常簡(jiǎn)單,就是說(shuō)我們?cè)谄渌胤?簡(jiǎn)單的說(shuō)就是不針對(duì)屏幕,不顯示出來(lái)的地方)開(kāi)辟一個(gè)存儲(chǔ)空間,我們把所有的動(dòng)畫(huà)都要渲染到這個(gè)地方,而不是直接渲染到屏幕上(針對(duì)屏幕的存儲(chǔ)區(qū)域)。在GDI中,直接針對(duì)屏幕就是窗口DC,”不可見(jiàn)的地方”一般可以用Memory DC。在把所有動(dòng)畫(huà)渲染到后臺(tái)緩沖之后,再一下次整體拷貝到屏幕緩沖區(qū)!
在純軟件2D圖形引擎中,雙緩沖一般意味著在內(nèi)存中開(kāi)辟一個(gè)區(qū)域用來(lái)存儲(chǔ)像素?cái)?shù)據(jù)。而在DirectDraw中可以創(chuàng)建Back Surface,在把所有動(dòng)畫(huà)渲染到Back Suface上之后,然后使用Flip操作使其可見(jiàn),F(xiàn)lip操作因?yàn)橹皇窃O(shè)置可見(jiàn)surface的地址,所以非常快速。
讓我們重寫(xiě)一下void CMyView::RenderView()函數(shù),來(lái)用GDI實(shí)現(xiàn)雙緩沖:
void CMyView::RenderView()
{
CClientDC dc(this);
CRect rc;
GetClientRect(rc);
CDC memdc;
memdc.CreateCompatibleDC(&dc);
CBitmap bmp;
Bmp. CreateCompatibleBitmap (&dc,rc.Width(),rc.Height());
CBitmap *oldbmp=memdc.SelectObject(&bmp);
Static int curframe=0;
m_myimglist.Draw(&memdc,curframe,Cpoint(0,0),ILD_TRANSPARENT);
curframe++;
If(curframe > m_myimglist.GetImageCount())
Curframe=0;
if(oldbmp)
memdc.SelectObject(oldbmp);
dc.BitBlt(0,0,rc.Width(),rc.Height(),&memdc,0,0,SRCCOPY);
}
其中創(chuàng)建一個(gè)Bitmap對(duì)象,然后選入Memory DC是必須的,因?yàn)镃reateCompatibleDC所創(chuàng)建的DC里面只含有一個(gè)1*1像素的單色Bitmap對(duì)象,所以如果缺了這個(gè)步驟,任何在MemoryDC上的繪圖操作都會(huì)沒(méi)有效果。延伸出一個(gè)問(wèn)題, CreateCompatibleBitmap函數(shù)的第一個(gè)參數(shù)顯然不可寫(xiě)成&memdc,如果那樣的化,你就創(chuàng)建了一個(gè)單色的位圖,我想你肯定不希望這樣。J
重寫(xiě)后的函數(shù)看上去似乎多了很多無(wú)謂的操作,這是因?yàn)槲覀儸F(xiàn)在只有一個(gè)動(dòng)畫(huà)對(duì)象,如果我們有多個(gè)動(dòng)畫(huà),而且還需要繪制動(dòng)畫(huà)的子窗口,那這樣做的效果就會(huì)非常的好,不會(huì)有任何閃爍,而且向文章最后提到的圖形MUD客戶端,還能達(dá)到60FPS呢(在我家的賽陽(yáng)433上)。
到此為止,我們的基本動(dòng)畫(huà)系統(tǒng)已經(jīng)有了一個(gè)很好的基礎(chǔ)了。
透明色(color key)處理
透明色就是指在繪制一張圖片的時(shí)候,該顏色的像素不會(huì)被繪制上去,這通常用來(lái)做游戲的spirit動(dòng)畫(huà),所以你可以看到各種形狀不規(guī)則的人物動(dòng)畫(huà)。但是他們的數(shù)據(jù)都是一個(gè)矩形的像素區(qū)域,只是繪制的時(shí)候有些像素不被畫(huà)上去罷了。
GDI提供一個(gè)TransparentBlt()函數(shù)來(lái)支持Color Key,你可以在MSDN中查到該函數(shù)的說(shuō)明。但是我的代碼中使用這個(gè)函數(shù)后,在Win9X系統(tǒng)下產(chǎn)生了嚴(yán)重的資源泄漏,但是在Win2000下卻沒(méi)事,所以如果你也發(fā)現(xiàn)這問(wèn)題的話,我建議你使用下面的代碼,來(lái)把一個(gè)CBitmap透明的繪制到DC上。假設(shè)你有一個(gè)CBitmap的派生類(lèi)CMyBitmap:
BOOL CMyBitmap::DrawTransparentInPoint(CDC *pdc, int x, int y, COLORREF mask/*要過(guò)濾掉的顏色值*/)
{
file://Quick return
if(pdc->GetSafeHdc()==NULL)
return FALSE;
if (m_hObject == NULL)
return FALSE;
CRect DRect;
DRect=Rect();
DRect.OffsetRect(x,y);
if(!pdc->RectVisible(&DRect))
return FALSE;
COLORREF crOldBack=pdc->SetBkColor(RGB(255,255,255));
COLORREF crOldText=pdc->SetTextColor(RGB(0,0,0));
CDC dcimg,dctrans;
if(dcimg.CreateCompatibleDC(pdc)!=TRUE)
return FALSE;
if(dctrans.CreateCompatibleDC(pdc)!=TRUE)
return FALSE;
CBitmap *oldbmpimg=dcimg.SelectObject(this);
CBitmap bmptrans;
if(bmptrans.CreateBitmap(Width(),Height(),1,1,NULL)!=TRUE)
return FALSE;
CBitmap *oldbmptrans=dctrans.SelectObject(&bmptrans);
dcimg.SetBkColor(mask);
dctrans.BitBlt(0,0,Width(),Height(),&dcimg,0,0,SRCCOPY);
pdc->BitBlt(x,y,Width(),Height(),&dcimg,0,0,SRCINVERT);
pdc->BitBlt(x,y,Width(),Height(),&dctrans,0,0,SRCAND);
pdc->BitBlt(x,y,Width(),Height(),&dcimg,0,0,SRCINVERT);
if(oldbmpimg)
dcimg.SelectObject(oldbmpimg);
if(oldbmptrans)
dctrans.SelectObject(oldbmptrans);
pdc->SetBkColor(crOldBack);
pdc->SetTextColor(crOldText);
return TRUE;
}
Alpha混合
Alpha混合是一種像素混合的方法。所謂的像素混合就是使用一定的算法把兩個(gè)像素的值混合成一個(gè)新的像素值(倒,和沒(méi)說(shuō)一樣),通常我們都把兩個(gè)像素的值,分別叫做源(src)和目的(dst),然后把混合后的結(jié)果存入dst中:
dst= src blend dst
如果源像素和目的像素都是RGBA格式,你可以使用每個(gè)像素的Alpha信息(或者叫做Alpha通道)組合出各種運(yùn)算公式,例如
dst= src*src.alpha+dst*dst.alpha;
或者
dst=src*src.alpha + dst*(1-src.alpha)//這里我們假設(shè)alpha值是0~1的浮點(diǎn)數(shù)。
可惜標(biāo)準(zhǔn)GDI沒(méi)有支持類(lèi)似這種操作的函數(shù)(起碼我沒(méi)找到),它只支持另一種Alpha混合,我把它叫做const alpha blend,也就是把兩幅都不包含Alpha通道的圖像的按照一個(gè)固定的Alpha值混合到一起,也就是每個(gè)像素都使用同一Alpha值。GDI的支持這個(gè)操作的函數(shù)是:
AlphaBlend(
HDC hdcDest,
int nXOriginDest,
int nYOriginDest,
int nWidthDest,
int hHeightDest,
HDC hdcSrc,
int nXOriginSrc,
int nYOriginSrc,
int nWidthSrc,
int nHeightSrc,
BLENDFUNCTION blendFunction
);
這個(gè)API的參數(shù)個(gè)數(shù)略多了一些,但是我想其中的位置參數(shù)你可以輕松搞定,還有就是源DC和目的DC,當(dāng)然了,我們的GDI只能對(duì)DC操作,而不是對(duì)我們的像素?cái)?shù)據(jù),而我們只要把我的位圖select到DC中就OK了,最后一個(gè)參數(shù)是一個(gè)結(jié)構(gòu),是用來(lái)指定Alpha的運(yùn)算方式的,請(qǐng)看一個(gè)實(shí)際的例子:
BLENDFUNCTION bf;
bf.AlphaFormat=0;
bf.BlendFlags=0;
bf.BlendOp=AC_SRC_OVER;
bf.SourceConstantAlpha=100;//指明透明度,取值范圍是0~255
AlphaBlend(pdc->GetSafeHdc(),rc.left,rc.top,rc.Width(),rc.Height(),
memdc.GetSafeHdc(),0,0,rc.Width(),rc.Height(),bf);
也許你看過(guò)很多游戲,在彈出文字對(duì)話框的時(shí)候都是在游戲畫(huà)面上蒙一層半透明的黑色,然后在這上面印字。使用上述操作就可以達(dá)到此效果。你可以先建立一個(gè)Memory DC,然后把他填充為黑,然后把Alpha值設(shè)為128,然后混合到你要繪制的DC上(不一定是窗口DC哦,記得我們前面將的雙緩沖嗎?)就OK了。
讀取JPEG,GIF文件
JPEG壓縮算法綜合的信號(hào)學(xué)和視覺(jué)心理學(xué),而GIF格式,特別是支持動(dòng)畫(huà)的GIF89a格式為了節(jié)約容量也做了很多種非常變態(tài)的優(yōu)化,所以要寫(xiě)一個(gè)完全支持這些標(biāo)準(zhǔn)格式的解碼器相當(dāng)困難,也沒(méi)有必要。
如果你需要進(jìn)行JPEG文件的讀寫(xiě)我推薦你使用Intel Jpeg Lib,速度相當(dāng)令人滿意。而GIF由于授權(quán)問(wèn)題,沒(méi)有任何官方組織提供的讀寫(xiě)代碼。
如果你只是需要讀入JPEG和靜態(tài)GIF(或者只一幀的動(dòng)態(tài)GIF),我推薦你使用Windows提供的OleLoadPicture函數(shù),下面這段代碼可以把一個(gè)JPG,GIF,BMP讀入到Bitmap對(duì)象中:
BOOL CIJLBitmap::Load(LPCTSTR lpszPathName)
{
BOOL bSuccess = FALSE;
file://Free up any resource we may currently have
DeleteObject();
file://open the file
CFile f;
if (!f.Open(lpszPathName, CFile::modeRead))
{
TRACE(_T("Failed to open file %s, Error:%x\n"), lpszPathName, ::GetLastError());
return FALSE;
}
file://get the file size
DWORD dwFileSize = f.GetLength();
file://Allocate memory based on file size
LPVOID pvData = NULL;
HGLOBAL hGlobal = GlobalAlloc(GMEM_MOVEABLE, dwFileSize);
if (hGlobal == NULL)
{
TRACE(_T("Failed to allocate memory for file %s, Error:%x\n"), lpszPathName, ::GetLastError());
return FALSE;
}
pvData = GlobalLock(hGlobal);
file://ASSERT(pvData);
if(pvData==NULL)
{
TRACE(_T("Failed to lock memory\r\n"));
return FALSE;
}
// read file and store in global memory
if (f.Read(pvData, dwFileSize) != dwFileSize)
{
TRACE(_T("Failed to read in image date from file %s, Error:%x\n"), lpszPathName, ::GetLastError());
GlobalUnlock(hGlobal);
GlobalFree(hGlobal);
return FALSE;
}
file://Tidy up the memory and close the file handle
GlobalUnlock(hGlobal);
file://create IStream* from global memory
LPSTREAM pStream = NULL;
if (FAILED(CreateStreamOnHGlobal(hGlobal, TRUE, &pStream)))
{
TRACE(_T("Failed to create IStream interface from file %s, Error:%x\n"), lpszPathName, ::GetLastError());
GlobalFree(hGlobal);
return FALSE;
}
// Create IPicture from image file
if (SUCCEEDED(::OleLoadPicture(pStream, dwFileSize, FALSE, IID_IPicture, (LPVOID*)&m_pPicture)))
{
short nType = PICTYPE_UNINITIALIZED;
if (SUCCEEDED(m_pPicture->get_Type(&nType)) && (nType == PICTYPE_BITMAP))
{
OLE_HANDLE hBitmap;
OLE_HANDLE hPalette;
if (SUCCEEDED(m_pPicture->get_Handle(&hBitmap)) &&
SUCCEEDED(m_pPicture->get_hPal(&hPalette)))
{
Attach((HBITMAP) hBitmap);
m_Palette.Attach((HPALETTE) hPalette);
bSuccess = TRUE;
}
}
}
file://Free up the IStream* interface
pStream->Release();
return bSuccess;
}
這個(gè)class的完整代碼請(qǐng)看文章最后的參考。
子窗口管理
你也許注意過(guò)幾乎所有游戲界面中的窗口都是使用動(dòng)畫(huà)的從屏幕外飛出(而且是半透明的,這你已經(jīng)可以做到了)。游戲中一般都使用自己的UI系統(tǒng)。這里我們可以借助Windows對(duì)窗口的管理來(lái)輕松實(shí)現(xiàn)各種動(dòng)畫(huà)子窗口。
首先讓我們從最簡(jiǎn)單的開(kāi)始。假設(shè)在我們的動(dòng)畫(huà)窗口中需要一個(gè)漂亮的按鈕怎么辦,我勸你最好不要使用CBitmapButton,因?yàn)槟阋呀?jīng)上了每秒重畫(huà)窗口16次以上這條賊船,我建議你在每次重畫(huà)父窗口的時(shí)候重畫(huà)所有子窗口,如此一來(lái)子窗口上如果要求有動(dòng)畫(huà)操作,也可以輕松實(shí)現(xiàn)了。既然做了,就把它做到最好。J
那我們?cè)趺炊x一個(gè)button呢?你也許想到自己定義一個(gè)矩形區(qū)域,然后在父窗口的消息響應(yīng)函數(shù)中檢測(cè)是否是對(duì)此區(qū)域操作,這樣在重畫(huà)父窗口的時(shí)候特殊的畫(huà)一次這個(gè)矩形區(qū)域就好了。這樣是可以實(shí)現(xiàn),但是顯然不符合我們的OOP精神,界面元素一多,你很可能就會(huì)亂了陣腳。最后的解決方法當(dāng)然是使用我們可愛(ài)的CWnd類(lèi),顯然所有的界面元素都可以作為一個(gè)CWnd派生類(lèi)的對(duì)象。不過(guò)我建議你不要從CButton派生,這帶來(lái)的麻煩遠(yuǎn)多于它的價(jià)值。從CWnd派生一個(gè)類(lèi),然后在Create時(shí)注意使用WS_CHILD風(fēng)格,并且指定父窗口為我們的動(dòng)畫(huà)窗口。
下面一個(gè)問(wèn)題是如何調(diào)用這些子窗口重畫(huà)操作呢?第一種較好的解決方法是先建立這樣一個(gè)虛基類(lèi):
CmyAniWnd :public CWnd
{
…
virtual void Render(CDC *pdc)=0;
…
}
假設(shè)你有一個(gè)Button類(lèi)和一個(gè)TextBox類(lèi):
CmyButton : public CmyAniWnd
CmyTextBox: public CmyAniWnd
這兩個(gè)類(lèi)都必須實(shí)現(xiàn)Render函數(shù),這樣在父窗口類(lèi)中你可以保存一個(gè)指針數(shù)組,例如這樣:
CPtrArray m_allchild;
在創(chuàng)建一個(gè)Button時(shí)這樣寫(xiě):
CmyButton *pbtn=new CmyButton;
m_allchild.Add(pbtn);
pbtn->Create(…);
然后在我們父窗口的RenderView函數(shù)(前面提到的,每次更新調(diào)用)中這樣寫(xiě)即可:
CmyAniWnd *pchild=NULL;
for(int I;I<m_allchild.GetSize();I++)
{
pchild=static_cast<CmyAniWnd*>(m_allchild.GetAt(i));
ASSERT(::IsWindow(pchild->GetSafeHwnd());
pchild->Render(&memdc);
}
這是一個(gè)典型的虛函數(shù)的應(yīng)用,在調(diào)用這些子窗口的Render函數(shù)時(shí),我們不需要知道它到底是Button還是TextBox,虛函數(shù)機(jī)制會(huì)自動(dòng)幫我們找到該調(diào)用的函數(shù)。還有一點(diǎn)就是,請(qǐng)注意,一定要把子窗口渲染到我們的后臺(tái)緩沖,也就是Memory DC中,否則還是會(huì)閃爍的。
上面這種方法適合于子窗口數(shù)目固定,更高級(jí)的界面會(huì)要求觸發(fā)某個(gè)事件的時(shí)候產(chǎn)生一個(gè)子窗口,子窗口不斷更新自己,并且在適當(dāng)?shù)臅r(shí)候把自己從UI系統(tǒng)中去除。讓每個(gè)子窗口管理自己的生命期,是個(gè)不錯(cuò)的主意,不是嗎?那你最好不要使用上面保存指針數(shù)組的方法,那樣的話,子窗口在殺死自己的時(shí)候還要通知父窗口,以讓父窗口把它的指針從數(shù)組中移除,這顯然具有很高的偶合性,不是我們想要的。因?yàn)槲覀兊乃凶哟翱诙际菢?biāo)準(zhǔn)的Windows對(duì)象,所以這使得我們有使用Windows消息的機(jī)會(huì)。我們首先要枚舉所有子窗口,然后發(fā)一個(gè)自定義的更新消息給它,并把我們的MemoryDC的指針作為參數(shù),具體例子代碼如下:
void CMyView::RenderView()
{
…//其它更新操作
::EnumChildWindows(GetSafeHwnd(),CMyView::UpdateChildWnd,LPARAM(&memdc));
…//其它更新操作
}
其中第二個(gè)參數(shù)是一個(gè)回調(diào)函數(shù),你必須把它聲明成全局函數(shù),或者類(lèi)的static成員函數(shù),這里我們使用了后者。
BOOL CALLBACK CMyWnd::UpdateChildWnd(HWND hwnd, LPARAM lParam/*CDC* */)
{
::SendMessage(hwnd,WM_COMMAND,CHILDCMD_RENDER,lParam);
return TRUE;
}
這里我沒(méi)有使用自定義消息,而是發(fā)送標(biāo)準(zhǔn)的WM_COMMAND,這樣你可以給那個(gè)CmyAniWnd虛基類(lèi)添加一個(gè)CWnd虛函數(shù)OnCommand(),然后在那里面檢測(cè)如果wParam是CHILDCMD_RENDER的話,就調(diào)用純虛函數(shù)Render(以lParam作為參數(shù)),子窗口派生類(lèi)只要實(shí)現(xiàn)自己的Render函數(shù)就好,其它不用管了。
這里還有一個(gè)要注意的問(wèn)題就是繪制的順序問(wèn)題,如果你想讓子窗口蓋住某些動(dòng)畫(huà),就應(yīng)該先渲染那些動(dòng)畫(huà),然后渲染子窗口,反之亦反。
進(jìn)階技巧--使用DIB
像素操作
以上所有操作都局限于標(biāo)準(zhǔn)GDI函數(shù),如果我們要實(shí)現(xiàn)更進(jìn)一步的操作,例如當(dāng)傍晚你希望把整個(gè)畫(huà)面的顏色渲染能淡紅色調(diào),晚上的時(shí)候你要把整個(gè)畫(huà)面變暗,早上再把它恢復(fù)到原來(lái)的亮度這些GDI都無(wú)法幫你做到。 如果想達(dá)到上述效果,就必須自己對(duì)像素的RGB值進(jìn)行操作。
首先讓我們要得到一個(gè)Bitmap對(duì)象中的像素?cái)?shù)據(jù)。讓我們看一下具體該怎么操作。假設(shè)我們有一個(gè)mybmp是一個(gè)CBitmap對(duì)象(或者其派生類(lèi)對(duì)象),下面的代碼把CBitmap中的像素取出:
BITMAP bm;
mybmp.GetBitmap(&bm);
BITMAPINFO binfo;
ZeroMemory(&binfo,sizeof(BITMAPINFO));
binfo.bmiHeader.biBitCount=24; file://24bit像素格式
binfo.bmiHeader.biCompression=0;
binfo.bmiHeader.biHeight=-bm.bmHeight;
binfo.bmiHeader.biPlanes=1;
binfo.bmiHeader.biSizeImage=0;
binfo.bmiHeader.biSize=sizeof(BITMAPINFOHEADER);
binfo.bmiHeader.biWidth=bm.bmWidth;
CClientDC dc(this);
BYTE *pbuf;//用來(lái)存儲(chǔ)像素?cái)?shù)據(jù)
int linebytes=(bm.bmWidth*3+3)&(~3);//4字節(jié)對(duì)齊
int size=linebytes*bm.bmHeight;
pbuf=new BYTE[size];
::GetDIBits(dc,m_bmpSword,0,bm.bmHeight,pbuf,&binfo,DIB_RGB_COLORS);上面代碼執(zhí)行后,我們的pbuf中就存儲(chǔ)了從mybmp拷貝而來(lái)的像素?cái)?shù)據(jù),而且是24bit模式的,這樣你就可以對(duì)所有這些像素進(jìn)行你所需要的操作了,例如晚上了,你想把這個(gè)Bitmap變暗,我這里粗略的把每個(gè)像素的RGB值都降低一半,可以使用下面的循環(huán):
for(int I;I<size;I++)
pbuf[i]=pbuf[i]/2;
得到了像素你就得到了一切,所有操作你都可以進(jìn)行,例如上面提到的標(biāo)準(zhǔn)GDI不支持的Alpha通道。
呵呵,像素交給你了,這樣我就放心了,那我走了…。。等等,你得到了這些像素,但是渲染時(shí)我們還是要使用標(biāo)準(zhǔn)GDI操作,所以好把這些像素設(shè)置回Bitmap對(duì)象中才行,好吧,這其實(shí)很簡(jiǎn)單,繼續(xù)上面的代碼:
SetDIBits(dc,mybmp,0,bm.bmHeight,pbuf,&binfo,DIB_RGB_COLORS);
最后別忘了:
delete[] pbuf;
RLE壓縮
現(xiàn)在的個(gè)人電腦內(nèi)存容量已經(jīng)非常大了,但是對(duì)某些人來(lái)說(shuō)還顯得不夠(或者他們不愿意浪費(fèi)這些可憐的資源雖然它們可再生),例如在Diablo中一個(gè)骷髏兵從地上站起來(lái)的動(dòng)畫(huà)為96*96像素*100幀,所以你有很多這樣的動(dòng)畫(huà),最好壓縮一下.
RLE是游戲常用的技巧,但是似乎已經(jīng)超出了本文的范圍。而且這方面的文章很多,我這里就不贅述了,留給你自己去進(jìn)一步發(fā)掘.J
最后,更多編程文章,請(qǐng)?jiān)L問(wèn)我個(gè)人網(wǎng)站http://www.diamondgarden.net/。
參考
華山論鍵
2001年上半年,我為號(hào)稱(chēng)國(guó)內(nèi)最大武俠社區(qū)的笑傲江湖.com實(shí)現(xiàn)的圖形MUD客戶端軟件,基于上述技術(shù)。詳情請(qǐng)見(jiàn)http://hslj.Xajh.com
其它類(lèi)庫(kù)
CIJLBitmap 一個(gè)CBitmap的派生類(lèi),可以Load BMP,JPG,GIF文件
NewImage Lib 純軟件2D圖像引擎,支持RLE,Alpha通道等,與GDI和DX無(wú)關(guān),所謂的Open-ending。
以上兩個(gè)都可以到我個(gè)人網(wǎng)站http://www。diamondgarden。net下載。
鄭重聲明:本文所有使用的所有圖片,其版權(quán)都?xì)w笑傲江湖.Com(http://www.xajh.com)武俠文化社區(qū)所有!不得擅自使用,否則責(zé)任自負(fù),與本文作者無(wú)關(guān)。