相信大家都玩過或見過一些絢麗的3D遊戲,也都知道其中的人物和情境是使用各種3D軟體——諸如3DSMAX、MAYA製作完成的。那這些作品是如何被用在程式中的呢?我們熟知的Direct3D就是用來做這件事的。本篇將簡要介紹如何通過D3D來運用3D作品。不要怕沒有接觸過D3D就不看了,筆者將從最簡單的——如何建立3D裝置談起。
下載原始碼
注意,除了DirectX9.0SDK外,我們還需要一個conv3ds轉換器,在D3D的外掛程式工具包裡。D3D中一個3D物體——包含了頂點資訊、材質、貼圖、動畫楨的3D成品被叫做mesh物體。conv3ds就是用來把3D作品轉換為標準mesh物體檔案(*.x)的。
言歸正傳,首先你要有一個像樣的3D作品。
開啟你的3DSMAX(或MAYA等其他3D軟體要匯出D3D能用的檔案,還需要另外的外掛程式,這裡就不多說了),盡情發揮你的才智來製作任何你想要的模型。不過醜話說在前面,熟悉3D製作的朋友都清楚,精緻的3D作品要靠大量的各種類型的貼圖來完成,但D3D只認Diffuse(表面色)一個通道。儘管它也支援Bump(凹凸)Specular Color(高光色)等另外的一些,但都不能直接用3D軟體來完成,另需代碼。這也就是為什麼相對3D作品來說,遊戲的渲染速度要快的多(當然還有其他原因),也是為什麼遊戲的畫質都不夠傳神。好吧,盡量做得好看吧。還要注意,貼圖的長和寬一定要是2的冪次像素大小,並且和你的程式在同一目錄下。筆者製作了一個放飛的標誌作為例子,存為"C:/frontfree.3ds",並使用了一張名為"C:/larglog.bmp"和一張名為"C:/Lakerrem2.jpg"的貼圖。
在菜單file中用export(匯出整個情境)或export selecte(只匯出所選物體)將其匯出為*.3ds檔案。然後用命令列方式運行conv3ds把它轉*.x。這樣就有了一個可用的3D作品。conv3ds有很多有用的參數,讀者朋友可以在包裡找到它的讀我檔案,這裡只介紹一個-x(小寫)。預設的*.x都是二進位的,這樣寫就產生文本式的*.x:conv3ds -x *.x,用UtralEdit等開啟,你會直接看到mesh物體是如何構成的,當然,這樣的檔案要大一些。
該開始寫程式了。一般來說,D3D程式要用到以下標頭檔:D3DX9.H,D3D9.H,D3DTYPES.H,以及相應的庫檔案,還有個WINMM.LIB。
不管要做什麼,建立一個視窗,主事件迴圈等等總是Windows程式首要的且一成不變的。下面的程式碼完成了這些工作:
LRESULT CALLBACK WinProc( HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
switch ( msg )
{
case WM_DESTROY:
//...這裡將調用一個是收尾函數來釋放所有介面指標
PostQuitMessage(0);
DestroyWindow(hWnd);
break;
default:
break;
}
return ( DefWindowProc( hWnd, msg, wParam, lParam ) );
}
int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd )
{
//create a main window:
WNDCLASSEX mainwnd;
mainwnd.cbSize=sizeof(WNDCLASSEX);
mainwnd.cbClsExtra=0;
mainwnd.cbWndExtra=0;
mainwnd.hbrBackground=NULL;//這將是一個沒有背景
mainwnd.hCursor=NULL;//滑鼠是沙漏
mainwnd.hIcon=NULL;//沒有表徵圖
mainwnd.hIconSm=NULL;
mainwnd.hInstance=hInstance;
mainwnd.lpfnWndProc=WinProc;//沒有標題列
mainwnd.lpszClassName="main window";
mainwnd.lpszMenuName=NULL;//沒有菜單
mainwnd.style=CS_OWNDC;//的視窗
RegisterClassEx(&mainwnd);
HWND hWnd = CreateWindowEx( NULL,"main window","only a demo",
WS_POPUP|WS_VISIBLE, 0, 0, 800, 600, NULL, NULL, hInstance, NULL);
//在此初始化D3D裝置,物體
//Init_D3D();
//主事件迴圈:
MSG msg;
ZeroMemory(&msg,sizeof(msg));
while (msg.message!=WM_QUIT)
{
//proc message:
if ( PeekMessage( &msg, hWnd, 0, 0, PM_REMOVE ) )
{
TranslateMessage( &msg );
DispatchMessage( &msg );
}
else
{
//每幀一次的渲染就在這裡
//Render();
}
}
//UnRegiste the window:
UnregisterClass( "main window", mainwnd.hInstance );
return ( msg.wParam);
}//end winmain
Direct3D是DirectX中極其重要的組建,提供了一套完整的用來設定情境、操作3D物體、渲染畫面的方法。來複習一下D3D程式的一般流程:首先得有個要渲染的物體,你得給出它的頂點、貼圖、材質資訊。然後把它的座標(3D的)轉換為螢幕座標,再添加一些燈光資訊,然後調用一個渲染方法,它所做的就是根據各個頂點的位置(已是螢幕座標),用燈光和材質資訊計算亮度,把貼圖“填”進去,這樣你看起來就是個立體的東西。渲染方法函數把圖畫在後備緩衝,因此最後別忘了把它換到前緩衝。你要有個Dirext3D的介面作為建立一切的基礎。像這樣:
LPDIRECT3D9 g_lpd3d9;//全域的
g_lpd3d9 = Direct3DCreate9( D3D_SDK_VERSION );//參數代表了你的D3D SDK版本。必須要這樣寫
然後通過它來建立一個Direct3D裝置介面,其他所有建立物體、燈光,以及渲染等方法都要通過這個裝置介面指標來調用:
LPDIRECT3DDEVICE9 g_lpd3ddevice;//全域的
HRESULT InitD3D(HWND hWnd)
{
D3DPRESENT_PARAMETERS m_d3dpp;
//我們要把當前適配器(顯卡)的資訊讀到這個結構體裡,它將被用於建立裝置
D3DDISPLAYMODE m_d3ddm;
g_lpd3d9->GetAdapterDisplayMode(D3DADAPTER_DEFAULT,&m_d3ddm);
//D3DADAPTER_DEFAULT表示預設顯卡
ZeroMemory(&m_d3dpp,sizeof(m_d3dpp));
m_d3dpp.Windowed=false;//要建立一個全屏程式,則置此項為FALSE
m_d3dpp.BackBufferWidth=m_d3ddm.Width;
m_d3dpp.BackBufferHeight=m_d3ddm.Height;
m_d3dpp.BackBufferFormat=m_d3ddm.Format;//此項記錄色深資訊
m_d3dpp.SwapEffect=D3DSWAPEFFECT_DISCARD;
m_d3dpp.EnableAutoDepthStencil = TRUE;//讓D3D管理深度緩衝
m_d3dpp.AutoDepthStencilFormat=D3DFMT_D16;//設定深度緩衝為16位
m_d3dpp.hDeviceWindow=hWnd;
//create the device:
g_lpd3d9->CreateDevice(D3DADAPTER_DEFAULT,//適配器標識符,大多數情況使用預設的
D3DDEVTYPE_HAL,//硬體抽象層
hWnd,//這是一個當前視窗的控制代碼,使用剛才CreateWindowEx()的傳回值就行了
D3DCREATE_HARDWARE_VERTEXPROCESSING,//頂點處理類型,這裡使用硬體處理
&m_d3dpp,//描述裝置的結構體
&g_lpd3ddevice);//最後給一個要被建立的裝置的指標
//啟用Z緩衝
g_lpd3ddevice->SetRenderState(D3DRS_ZENABLE,TRUE);
return S_OK;
}
Z緩衝是深度緩衝的一種。深度緩衝是一塊與螢幕大小相同的地區,記錄著螢幕上每個點的“最近像素”有多“近”。比如說程式要畫一個三角形中的某個點,但它發現深度緩衝中記錄的距離比這個點要近,也就是說其他物體的某點擋在它前面,自然也就不會去畫它;否則就畫它,然後把該點的深度資訊寫進緩衝。現在我們把Z-Buffer開啟,就是把這些事交給系統處理。調節緩衝資料位元數是在較精確的資料與較大的空間之間作個權衡。
該用到剛剛做完的那個東西了。在D3D中,可以說所有物體的外形位置由頂點構成;材質決定著物體如何反射光線;貼圖的作用自不言而喻。任何3D程式首先要做的就是建立一套這些資訊。剛才說道,現在這些資訊都包含在現成的mesh物體中,我們要做的就是把它從檔案中提取出來。D3D中的IDirect3DMesh介面提供了這些方法。
LPD3DXMESH g_lpmesh;//全域的
mesh物體的定點資訊都存放在g_lpmesh裡,而材質和貼圖還要單獨提取出。D3D裡,材質和貼圖分別用D3DMATERIAL9結構體和ID3DTexture9介面描述。一般來說,mesh中都含有多種貼圖和材質,我們要將其聲明為數組。
D3DMATERIAL9 *g_meshmtrl;
LPDIRECT3DTEXTURE9 *g_meshtex;
數組的大小事先並不知道,取決於mesh物體的SebSet的數量。在一個mesh物體中,擁有相同材質貼圖屬性的一組三角形叫它的一個SubSet。因為轉換當前材質和貼圖是十分耗時的,因此在D3D中是按照一個SubSet組接著另一個去渲染的,這樣就避免了材質貼圖的頻繁轉變。我們用一個DWOD型的全域變數g_meshmtrlnum記錄這個mesh中的SubSet數量。
HRESULT InitGeometry()
{
LPD3DXBUFFER lpmeshmtrlbuffer;//材質貼圖的臨時緩衝
//從檔案裝載一個mesh物體:
D3DXLoadMeshFromX("c://", //檔案路徑
D3DXMESH_SYSTEMMEM,//g_lpmesh被建立的位置,這裡它被建立在系統記憶體而非顯示記憶體
g_lpd3ddevice,NULL,&lpmeshmtrlbuffer,NULL,
&g_meshmtrlnum,//一個用於存放SubSet數量的變數指標
&g_lpmesh);//最後是要被建立的mesh指標
//我們要一次性讀取出全部材質和貼圖資訊,並都存放在這裡:
D3DXMATERIAL *pd3dxmtrl=(D3DXMATERIAL*)lpmeshmtrlbuffer->GetBufferPointer();
//然後再細分:
g_meshmtrl=new D3DMATERIAL9[g_meshmtrlnum];
g_meshtex=new LPDIRECT3DTEXTURE9[g_meshmtrlnum];
//對每個SubSet,我們分別提取它的材質和貼圖
for(DWORD i=0;i<g_meshmtrlnum;i++)
{
g_meshmtrl[i]=pd3dxmtrl[i].MatD3D;
//實際上,pd3dxmtrl[i].pTextureFilename只是個檔案名稱
TCHAR texname[512];//存放完整的貼圖檔案路徑
strcpy(texname,"c://");
strcat(texname, pd3dxmtrl[i].pTextureFilename);
D3DXCreateTextureFromFile(g_lpd3ddevice,texname,&g_meshtex[i]);
}
lpmeshmtrlbuffer->Release();
lpmeshmtrlbuffer = NULL;
return S_OK;
}
開啟*.x檔案(如果是文字模式的)你會看到貼圖在檔案中只被記錄該圖片的檔案名稱。而CreateTextureFromFile()所需要的參數是一個完整路徑,所以如果這些圖不在程式同一目錄下,就要在代碼中做些處理。否則我敢保證你什麼也看不見。這裡,我只是簡單的使用了絕對路徑。
好,所有資訊都被從檔案中摘抄了出來,但現在還不是渲染的時候。沒有進行座標轉換,目前所有頂點使用的,都是其局部的3D座標,並非渲染函數能夠直接使用的螢幕座標。通常需要經過以下三種座標轉換:
全域轉換 將所有的物體轉換為統一的全域座標,你還可以在這裡完成各種對物體位置的操作。
視圖轉換 轉為從觀察者(玩家)角度看到的全域座標。先在全域座標系的指定位置架一台攝影機,指定一個注視點和一個參考點。座標將被轉換到以攝影機為原點,從原點到注視點為Z軸,再加上參考點,所確定的平面為Y-Z平面的座標系中。實際上就是把全域座標擰了個兒。
透視轉換 前兩種座標轉換並無本質區別,有些情況就把它們合為一個。而經過透視轉換,各頂點的X,Y值將表示實際的螢幕座標,而Z值就是Z-Buffer裡用到的深度資訊。
任何的基本的座標轉換(平移、旋轉、放縮),在3D程式裡,都是將該點(就是一個列向量)乘以一個4x4矩陣。上面的三種轉換實際就是一系列的基本轉換。矩陣不用讀者自己填寫,D3D會完成這些。然後還要調用相應的三個函數告訴D3D:你要用這個矩陣完成全域轉換,然後用那個矩陣完成視圖轉換等等。代碼如下:
HRESULT SetMatrix()
{
D3DXMATRIX matWorld;
//以下用來讓物體轉起來:
UINT iTime = timeGetTime() % 10000;
FLOAT fAngle = iTime * (2.0f * D3DX_PI) / 10000.0f;
D3DXMatrixRotationX( &matWorld,fAngle);//填充matWorld為一個旋轉矩陣,fAngle是旋轉角度
g_lpd3ddevice->SetTransform(D3DTS_WORLD,&matWorld);//將matWorld設定為世界轉換矩陣
D3DXMATRIX matView;
D3DXVECTOR3 d3dvEye(0,100,-500);//玩家所在位置,用一個D3DXVECTOR3描述
D3DXVECTOR3 d3dvLA(0,0,0);//視點
D3DXVECTOR3 d3dvUp(0,1,0);//參考點
D3DXMatrixLookAtLH( &matView,&d3dvEye,&d3dvLA,&d3dvUp );//填充matView矩陣
g_lpd3ddevice->SetTransform(D3DTS_VIEW,&matView);//設定為試圖轉換矩陣
D3DXMATRIX matProj;D3DXMatrixPerspectiveFovLH( &matProj,
D3DX_PI/4,//視角的範圍,一般是45度
float(1280)/float(1024),//這一項是視圖的長寬比
1.0f,10000.0f);//這兩個數是最近與最遠的可見範圍
g_lpd3ddevice->SetTransform(D3DTS_PROJECTION,&matProj);//設定為透視轉換矩陣
return S_OK;
}
可以渲染了,不過最好先來放置兩盞燈。燈在D3D裡用D3DLIGHT9結構體表示。熟悉3D製作的讀者不難猜出這些結構的成員。我設定了一盞白色的主光和一盞黃色的副光,注意,燈光並不會和物體一起轉動,全域轉換隻是針對物體的頂點。
HRESULT SetLight()
{
D3DLIGHT9 light_r;
ZeroMemory(&light_r,sizeof(light_r));
light_r.Type=D3DLIGHT_DIRECTIONAL;
//D3D裡支援泛光燈,聚光燈和平行光三種光源,這裡用了平行光
light_r.Diffuse.r=light_r.Diffuse.g=light_r.Diffuse.b=1;
light_r.Specular.r=light_r.Specular.g=light_r.Specular.b=1;
light_r.Direction=D3DXVECTOR3(0.5f,-0.5f,0.5f); //平行光的方向
g_lpd3ddevice->SetLight(0,&light_r);
//你可以設定很多的燈,第一個參數就是你要設定的燈的索引號,第二個參數就是描述這盞燈的結構體
lpd3ddevice->LightEnable(0,TRUE);
//然後讓這盞燈起作用,就好像把它開啟
D3DLIGHT9 light_b;
ZeroMemory(&light_b,sizeof(light_b));
light_b.Type=D3DLIGHT_DIRECTIONAL;
light_b.Diffuse.r=0.5;
light_b.Diffuse.g=1;
light_b.Direction=D3DXVECTOR3(-0.5F,-0.5F,0.5F);
g_lpd3ddevice->SetLight(1,&light_b);
g_lpd3ddevice->LightEnable(1,TRUE);
g_lpd3ddevice->SetRenderState(D3DRS_LIGHTING,TRUE);
return S_OK;
}
D3D中,物體的高光色並不依賴於其表面色,因此表面色和高光色可以分別設定。燈光也是這樣。我把第一盞燈的表面色和高光色都設定呈白色,而第二盞燈不參與高光運算,就好像環境反射光。光的顏色通過rgb來設定,每種顏色的範圍是0-1。
最後的渲染實際很簡單。首先要調用Clear()清除畫面和深度緩衝,把所有的SebSet畫到後備緩衝,然後調用Present()換到前緩衝,就可以出現在顯示器上了。
HRESULT Render()
{
g_lpd3ddevice->Clear(0,NULL,D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER,
//需要渲染的表面,這裡是目標畫面和深度緩衝
0x00000000,//用來清屏的顏色值,這裡是用黑色
1,0);
g_lpd3ddevice->SetTextureStageState(0,D3DTSS_COLOROP,D3DTOP_MODULATE);
g_lpd3ddevice->BeginScene();//每次畫圖形之前,一定要調用這個方法
SetMatrix();
for(DWORD i=0;i<g_meshmtrlnum;i++){
g_lpd3ddevice->SetMaterial(&g_meshmtrl[i]);//參數就是所需的材質
g_lpd3ddevice->SetTexture(0,
//D3D支援多重貼圖,這個參數是要設定的貼圖序號,這裡只用第一重
g_meshtex[i]);
g_lpmesh->DrawSubset(i);//這個方法才開始真正的畫圖,參數就是mesh的子物體序號
}
g_lpd3ddevice->EndScene();//這是與BeginScene()對應的
g_lpd3ddevice->Present(NULL,NULL,NULL,NULL);//翻頁,一般來說,四個參數都是NULL
return 0;
}
這隻是一個很簡單的例子,代碼極其簡陋,沒有錯誤校正,沒有釋放介面指標(讀者一定要想著釋放,否著你的程式並不會真正結束),沒有使用者操作,關閉是Alt+F4。效果也並不複雜:
這個例子只是想告訴讀者一些基本的做法。由於筆者水平有限,很多問題沒也有敘述清除。歡迎各位讀者朋友與我交流經驗。
本文來自CSDN部落格,轉載請標明出處:http://blog.csdn.net/xu_haiqing/archive/2006/04/18/667878.aspx