您好,登錄后才能下訂單哦!
本系列文章由zhmxy555(毛星云)編寫,轉載請注明出處。
作者:毛星云(淺墨) 微博:@淺墨_毛星云郵箱: happylifemxy@163.com
I'm back~,這段時間大家久等了~
現在大家看到的,就是【Visual C++】游戲開發系列文章第三季中作為正式回歸的第一篇文章了。
在這篇文章中,我們一起詳細探討了游戲編程中公告板技術的方方面面,有“深入”的過程分析,也有“淺出”的大家喜聞樂見的使用方法幾步曲,最后依舊是一個注釋詳細的示例程序將這一節的內容融會貫通,且文章最后附有這篇博文配套示例程序的源代碼下載。
首先放一張截圖吧:
這里的3D人物,可就是一張圖片哦,而不是之前我們用的3D人物模型。
那么文章開頭還是先和大家嘮嘮家常。淺墨現在想說的是這周實在是太累了,天天在各種奇奇怪怪忽然冒出來的事情上耗費時間。比如這個周六周末,明明是好好的自由自配時間,卻忽然冒出來要連上兩天奇葩的課,從早上8點上到下午6點。不過還好淺墨機智地帶了電腦過去,在課堂上坐在不起眼的地方低頭為大家寫這篇博客,不然可能就不會有這篇文章的出生了。
然后寫這句話的時候時間是10月27號晚上8點22分,一個小時之前淺墨正拖著身心俱疲的身體回到宿舍,倒頭就睡,按照平常的習慣如果累成這樣,必然是回來之后簡單的洗澡然后倒頭就睡到第二天早上自然醒,但是考慮到答應了大家今天晚上要更新博客,所以短暫的補充精力之后,撐著眼皮,逼著自己起來為大家更新這篇博客。
不過,淺墨知道這樣的努力到最終肯定都是值得的,就像之前有一次每日一語里面的那個句子一樣:
“面對生活,我們沒有選擇,但是請始終相信,現在所經歷的一切,都有它存在的意義。”
再扯下去這就不是一篇技術文章了,那么,我們就開始正題。
一、公告板(Billboard)技術的概念和定位
公告板(Billboard)技術,也常譯為廣告牌技術,通常是利用多邊形總是朝向觀察者這一特性,來達到某些特定的效果。它是一種通過簡單方式實現許多沒有固定表面效果的一項非常實用的技術,它可以四兩撥千斤地完成很多神奇的效果。例如公告板技術加上紋理的Alpha混合與動畫在一起使用,可以實現很多沒有固定表面的特效,如煙霧、火焰、爆炸、能量盾、云彩、水滴,樹木,森林,喊話窗口,甚至NPC人物等等。
需要再次強調的是,公告板技術通常都是和紋理的Alpha混合一起使用的,這樣便能夠以極低的系統資源實現豐富的繪制效果。
關于地位,公告板技術在3D游戲中有著非常廣泛的應用,因為它可以極大地節省資源、提高游戲的流暢度,降低對3D游戲素材的依賴,以及降低對配置要求的門檻。
二、對公告板技術的情境理解
公告板的原理是使用兩個三角形組成的矩形顯示一張平面位圖,并且在顯示的過程中,該平面矩形的角度隨虛擬攝像機的觀察角度和位置的變化而變化。
在Direct3D應用中,公告板的實現方式通常是兩種,一種是讓某個平面始終對著虛擬攝像機,也就是平面模型的屏幕與觀察者的視線垂直(圖1)。
另一種是讓平面模型全部朝向投影空間的前屏幕,也就是平面模型與投影平面相平行(圖2)。
公告板最常見的例子是用作模擬3D樹木。想要模擬3D樹木往往不用真正的3D模型,而是用看上去很像3D樹木的2D圖像映射到一個矩形平面上。
如果我們向某個方向觀察一個映射到多邊形的樹木時,由于多邊形是朝某個固定方向的,此時能夠正確的觀察該樹木。但是當我們與樹木朝向之間有一個觀察角度的話,圖形在觀察者的視野中就會逐漸變窄。
但是如果我們用了告板技術的話,那么樹木始終都是朝向用戶的,這個時候,無論我們如何在水平方向轉移觀察方向都可以正確地觀察到沒有很多異樣的樹木。
為了印象深刻,再來一個對比吧,如果不使用公告板技術,在水平方向上環繞著移動視角,便顯示出了扭曲的圖形:
使用公告板技術后,便正確顯示的人物畫面:
這樣的差距可不是一點點~
三、公告板技術的實現原理理解
然后是關于實現原理。
說白了,公告板技術的實現原理也就是三個方面需要注意:
1. 在合適的位置和朝向放置矩形平面
2. 讓矩形平面始終和視線垂直
3. 將位圖貼到矩形平面上
公告板技術的原理其實很簡單,就是使用兩個三角形組成的矩形來顯示一個位圖,在顯示過程中這個矩形面板根據攝像機的觀察角度和位置而變化。這里看起來又和前面章節中講到的在3D中顯示2D圖形有些像,但它們是不同的。當采用正交投影時,顯示出來的圖形將不具有縱深感,不會隨著距離的遠近而產生縮 放。而在3D環境中一般的面板又不能很好地顯示出效果。
一般來說,公告板技術可以分成三類,分別應用于不同的游戲類型需求:
1.觀察者可以在游戲虛擬空間中任意移動,而我們的工作是讓某一模型始終對著攝像機。例如,一個太陽。這一類在當前技術中使用較少,因為目前硬件的能力突飛猛進,直接放一個3D模型的太陽就行了。
2.在角色扮演游戲中,攝像機可以做的旋轉只限于水平和豎直。程序員的目的是:無論角色如何走動,某一個平面模型始終朝向觀察點。簡單來說,就是平面模型的平面和視線垂直。
a.垂直于視線圖示
(3)第三類和第二類相似,差異在于平面模型不是全部朝向觀察點,而是全部朝向投影空間的前屏幕。簡單來說,就是平面模型始終與屏幕平行。
b.平行于屏幕圖示
四、公告板技術使用五步曲
還是老規矩,深入淺出,沒力氣沒精力理解前面的那些理論的話(其實前面的理論也不難的- -),落實到一個用上,不妨直接啃這一節總結好的精粹知識,瞬間就學會了如何快速上手使用公告板技術。
關于這五步曲的大體思路,其實就是紋理映射幾步曲那一套弄完之后,渲染的時候加關于公告板矩陣的一些額外操作就是了。而最關鍵的兩個步驟:一是在合適的位置和朝向放置矩形平面,二是將位圖貼到矩形平面上。
Ⅰ.五步曲之一:頂點的定義
想實現公告板的話,首先當然是需要定義公告板矩形的靈活頂點格式FVF。因為在公告板矩形中只顯示一張位圖,所以在靈活頂點格式中僅需要包含位置和一對紋理坐標。
這一步當然是定義描述廣告牌矩形的靈活頂點格式。由于在廣告牌矩形上只用顯示一張位圖就行,所以在靈活頂點格式中只要包含位置和一對紋理坐標,但是其實我們之前在貼地面的紋理的時候就用到了相同格式的FVF頂點格式,所以如果在之前的demo上修改代碼的話,這步根本不用去新加什么代碼,知道我們之前用爛了的那個FVF頂點格式結構體CUSTOMVERTEX這次繼續服役就行了~
//定義FVF頂點結構 structCUSTOMVERTEX { FLOAT _x, _y, _z; FLOAT _u, _v ; CUSTOMVERTEX(FLOAT x, FLOAT y, FLOAT z,FLOAT u, FLOAT v) : _x(x), _y(y), _z(z), _u(u),_v(v) {} }; #defineD3DFVF_CUSTOMVERTEX (D3DFVF_XYZ |D3DFVF_TEX1)
Ⅱ、五步曲之二:頂點的訪問
填充頂點和之前的紋理映射步驟里的代碼除了頂點的坐標設置需要額外講究以外,其他的方面幾乎一模一樣。而需要講究的是放置公告板所在的矩形位置,也就是四個頂點的位置。最好是離是攝像機所在的位置遠一點,因為距離產生美嘛,畢竟紙是包不住火的,距離太近的話,無論是什么詭計,都會被識破的~
那么,關于公告板矩形頂點的設置,代碼如下:
//-----------------------------【創建NPC的頂點緩存】-------------------------------- g_pd3dDevice->CreateVertexBuffer(4 * sizeof(CUSTOMVERTEX), 0, D3DFVF_CUSTOMVERTEX,D3DPOOL_MANAGED, &g_pNPCVBuffer , NULL ); pVertices= NULL; g_pNPCVBuffer->Lock(0, 0, (void**)&pVertices, 0); pVertices[0]= CUSTOMVERTEX(-100.0f, 0.0f, 0.0f, 0.0f, 1.0f); pVertices[1]= CUSTOMVERTEX(-100.0f, 320.0f, 0.0f, 0.0f, 0.0f); pVertices[2]= CUSTOMVERTEX( 100.0f, 0.0f, 0.0f, 1.0f, 1.0f); pVertices[3]= CUSTOMVERTEX( 100.0f, 320.0f, 0.0f, 1.0f, 0.0f); g_pNPCVBuffer->Unlock();
即公告板矩形的四個點是(-100,0),(-100,320),(100,0),(100,320)。
Ⅲ、五步曲之三:紋理的創建
這步里面最好是找到一張看起來像3D的戴透明通道的圖片,這樣做出來的效果才會很真實。我們選取了一張游戲人物中的3D模型的渲染圖,看起來具有3D的效果,放置在工程目錄GameMedia\\girl.png之下。
// -----------------------------【創建NPC紋理】--------------------------------------- D3DXCreateTextureFromFile(g_pd3dDevice, L"GameMedia\\girl.png", &g_pNPCTexture );
Ⅳ、五步曲之四:構造并使用公告板矩陣
因為公告板技術是通過使用世界矩陣以及觀察點來排序公告板平面的,并且在程序中我們已經知道了觀察的角度或者可以獲得取景變換矩陣(我們后面的示例程序就是獲取了取景變換矩陣),而不用去改變公告板矩形的坐標位置。
這一步里面需要注意的是g_pCamera->CalculateViewMatrix(&matView); 這句里面計算當前的取景變換并存到matView中,用到了我們之前文章里講過的那個攝像機類CameraClass。如果你的程序里沒有使用淺墨寫的這個類的話,那么就根據自己的實際情況機智地去獲取好了。
//-----------------------------【利用公告板技術準備繪制NPC】-------------------------- //取得當前的取景變換矩陣 D3DXMATRIXmatView; g_pCamera->CalculateViewMatrix(&matView);
同樣需要注意的是,由于公告板始終是與攝像機的觀察方向垂直的,因此當攝像機的觀察方向改變時,同樣需要將公告板繞Y軸進行旋轉,因此,我們可以把取景變換矩陣的11、13和31、33向量分別設置到公告板的變換矩陣中,從而得到公告板在觀察坐標系中的變換矩陣。
一般的代碼可以是這樣的:
//根據取景變換矩陣來計算并構造公告板矩陣 D3DXMATRIX matBillboard; D3DXMatrixIdentity(&matBillboard); matBillboard._11 = matView._11; matBillboard._13 = matView._13; matBillboard._31 = matView._31; matBillboard._33 = matView._33;
而為了得到公告板在世界坐標系中的世界變換矩陣的話,還需要將公告板的變換矩陣做逆運算。
D3DXMatrixInverse(&matBillboard, NULL, &matBillboard);
最終,就是創建一個矩陣,右乘之前的公告板矩陣,并設置成世界矩陣
//創建一個矩陣,右乘之前的公告板矩陣,并設置成世界矩陣 D3DXMATRIX matNPC; D3DXMatrixIdentity(&matNPC); matNPC = matBillboard * matNPC; g_pd3dDevice->SetTransform(D3DTS_WORLD, &matNPC );
其實,創建公告板矩陣的另一種方法是通過當前的觀察方向完成。此時,將觀察點減去視點,得到觀察方向的向量,然后通過該向量讓公告板矩陣繞Y軸旋轉一定的角度。比如說這樣:
// 通過當前觀察方向來構造公告板矩陣 D3DXMATRIX matBillboard; D3DXVECTOR3 vDir = vAt - vEye; if( vDir.x > 0.0f ) D3DXMatrixRotationY( &matBillboard, -atanf(vDir.z/vDir.x)+D3DX_PI/2); else D3DXMatrixRotationY( &matBillboard, -atanf(vDir.z/vDir.x)-D3DX_PI/2);
這步這樣講有些雜亂了,我們用代碼把他們串起來:
//-----------------------------【利用公告板技術準備繪制NPC】-------------------------- //取得當前的取景變換矩陣 D3DXMATRIX matView; g_pCamera->CalculateViewMatrix(&matView); //根據取景變換矩陣來計算并構造公告板矩陣 D3DXMATRIX matBillboard; D3DXMatrixIdentity(&matBillboard); matBillboard._11 = matView._11; matBillboard._13 = matView._13; matBillboard._31 = matView._31; matBillboard._33 = matView._33; D3DXMatrixInverse(&matBillboard, NULL, &matBillboard); //創建一個矩陣,右乘之前的公告板矩陣,并設置成世界矩陣 D3DXMATRIX matNPC; D3DXMatrixIdentity(&matNPC); matNPC = matBillboard * matNPC; g_pd3dDevice->SetTransform( D3DTS_WORLD, &matNPC );
Ⅴ、五步曲之五:正式繪制
正式繪制的代碼應該就不用多做介紹了,之前那么多次已經耳濡目染了吧://-----------------------------【正式繪制NPC人物】----------------------------- g_pd3dDevice->SetTexture(0, g_pNPCTexture ); g_pd3dDevice->SetStreamSource(0, g_pNPCVBuffer , 0, sizeof(CUSTOMVERTEX) ); g_pd3dDevice->SetFVF(D3DFVF_CUSTOMVERTEX ); g_pd3dDevice->DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2 );
其實在正式繪制之前,往往還要做紋理映射狀態或者Alpha混合狀態的一些設置,瘋狂調用SetTextureStageState和SetSamplerState即可:
//設置紋理狀態 g_pd3dDevice->SetTextureStageState( 0, D3DTSS_COLOROP, D3DTOP_SELECTARG1); g_pd3dDevice->SetTextureStageState( 0, D3DTSS_COLORARG1, D3DTA_TEXTURE ); g_pd3dDevice->SetSamplerState( 0, D3DSAMP_MINFILTER, D3DTEXF_LINEAR ); g_pd3dDevice->SetSamplerState( 0, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR ); //Alpha混合設置, 設置混合系數 g_pd3dDevice->SetRenderState( D3DRS_ALPHABLENDENABLE, true ); g_pd3dDevice->SetRenderState( D3DRS_SRCBLEND, D3DBLEND_SRCALPHA ); g_pd3dDevice->SetRenderState( D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA );
嗯,概念似乎就是這么多了,下面依舊是老規矩,用一個示例程序來融會貫通這篇文章里面的主角“公告板技術”。
五、詳細注釋的示例程序源代碼
這次的示例程序依舊是在之前搭的那個框架之中,新增和修改了一些代碼,演化而來的。
好吧,上代碼,核心部分main.cpp的詳細注釋代碼:
//-----------------------------------【程序說明】---------------------------------------------- // 【Visual C++】游戲開發系列配套源碼五十四 淺墨DirectX教程二十一 視角上的詭計:公告板(Billboard)技術 // VS2010版 // 2013年10月 Create by 淺墨 // 背景音樂素材出處:火影忍者疾風傳 //------------------------------------------------------------------------------------------------ //-----------------------------------【宏定義部分】-------------------------------------------- // 描述:定義一些輔助宏 //------------------------------------------------------------------------------------------------ #define WINDOW_WIDTH 932 //為窗口寬度定義的宏,以方便在此處修改窗口寬度 #define WINDOW_HEIGHT 700 //為窗口高度定義的宏,以方便在此處修改窗口高度 #define WINDOW_TITLE _T("【致我們永不熄滅的游戲開發夢想】淺墨DirectX教程二十一 視角上的詭計:公告板(Billboard)技術 博文配套示例程序 by淺墨") //為窗口標題定義的宏 //-----------------------------------【頭文件包含部分】--------------------------------------- // 描述:包含程序所依賴的頭文件 //------------------------------------------------------------------------------------------------ #include <d3d9.h> #include <d3dx9.h> #include <tchar.h> #include <time.h> #include "DirectInputClass.h" #include "CameraClass.h" #include "SkyBoxClass.h" #include "SnowParticleClass.h" //-----------------------------------【庫文件包含部分】--------------------------------------- // 描述:包含程序所依賴的庫文件 //------------------------------------------------------------------------------------------------ #pragma comment(lib,"d3d9.lib") #pragma comment(lib,"d3dx9.lib") #pragma comment(lib, "dinput8.lib") // 使用DirectInput必須包含的庫文件,注意這里有8 #pragma comment(lib,"dxguid.lib") #pragma comment(lib, "winmm.lib") // 定義FVF頂點結構 struct CUSTOMVERTEX { FLOAT _x, _y, _z; FLOAT _u, _v ; CUSTOMVERTEX(FLOAT x, FLOAT y, FLOAT z, FLOAT u, FLOAT v) : _x(x), _y(y), _z(z), _u(u), _v(v) {} }; #define D3DFVF_CUSTOMVERTEX (D3DFVF_XYZ | D3DFVF_TEX1) //-----------------------------------【全局變量聲明部分】------------------------------------- // 描述:全局變量的聲明 //------------------------------------------------------------------------------------------------ LPDIRECT3DDEVICE9 g_pd3dDevice = NULL; //Direct3D設備對象 LPD3DXFONT g_pTextFPS =NULL; //字體COM接口 LPD3DXFONT g_pTextAdaperName = NULL; // 顯卡信息的2D文本 LPD3DXFONT g_pTextHelper = NULL; // 幫助信息的2D文本 LPD3DXFONT g_pTextInfor= NULL; // 繪制信息的2D文本 float g_FPS= 0.0f; //一個浮點型的變量,代表幀速率 wchar_t g_strFPS[50] ={0}; //包含幀速率的字符數組 wchar_t g_strAdapterName[60] ={0}; //包含顯卡名稱的字符數組 D3DXMATRIX g_matWorld; //世界矩陣 DInputClass* g_pDInput = NULL; //DInputClass類的指針實例 CameraClass* g_pCamera = NULL; //攝像機類的指針實例 SkyBoxClass* g_pSkyBox=NULL; //天空盒類的指針實例 SnowParticleClass* g_pSnowParticles = NULL; //雪花粒子系統的指針實例 LPDIRECT3DVERTEXBUFFER9 g_pFloorVBuffer = NULL; //地板頂點緩存對象 LPDIRECT3DTEXTURE9 g_pFloorTexture = NULL; //地板紋理對象 LPDIRECT3DVERTEXBUFFER9 g_pNPCVBuffer = NULL; // NPC頂點緩存對象 LPDIRECT3DTEXTURE9 g_pNPCTexture = NULL; // NPC紋理對象 D3DXMATRIX g_matView; //-----------------------------------【全局函數聲明部分】------------------------------------- // 描述:全局函數聲明,防止“未聲明的標識”系列錯誤 //------------------------------------------------------------------------------------------------ LRESULT CALLBACK WndProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam ); HRESULT Direct3D_Init(HWND hwnd,HINSTANCE hInstance); HRESULT Objects_Init(); void Direct3D_Render( HWND hwnd,FLOAT fTimeDelta); void Direct3D_Update( HWND hwnd,FLOAT fTimeDelta); void Direct3D_CleanUp( ); float Get_FPS(); void HelpText_Render(HWND hwnd); //-----------------------------------【WinMain( )函數】-------------------------------------- // 描述:Windows應用程序的入口函數,我們的程序從這里開始 //------------------------------------------------------------------------------------------------ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,LPSTR lpCmdLine, int nShowCmd) { //開始設計一個完整的窗口類 WNDCLASSEX wndClass={0} ; //用WINDCLASSEX定義了一個窗口類,即用wndClass實例化了WINDCLASSEX,用于之后窗口的各項初始化 wndClass.cbSize = sizeof( WNDCLASSEX ) ; //設置結構體的字節數大小 wndClass.style = CS_HREDRAW | CS_VREDRAW; //設置窗口的樣式 wndClass.lpfnWndProc = WndProc; //設置指向窗口過程函數的指針 wndClass.cbClsExtra = 0; wndClass.cbWndExtra = 0; wndClass.hInstance = hInstance; //指定包含窗口過程的程序的實例句柄。 wndClass.hIcon=(HICON)::LoadImage(NULL,_T("GameMedia\\icon.ico"),IMAGE_ICON,0,0,LR_DEFAULTSIZE|LR_LOADFROMFILE); //從全局的::LoadImage函數從本地加載自定義ico圖標 wndClass.hCursor = LoadCursor( NULL, IDC_ARROW ); //指定窗口類的光標句柄。 wndClass.hbrBackground=(HBRUSH)GetStockObject(GRAY_BRUSH); //為hbrBackground成員指定一個灰色畫刷句柄 wndClass.lpszMenuName = NULL; //用一個以空終止的字符串,指定菜單資源的名字。 wndClass.lpszClassName = _T("ForTheDreamOfGameDevelop"); //用一個以空終止的字符串,指定窗口類的名字。 if( !RegisterClassEx( &wndClass ) ) //設計完窗口后,需要對窗口類進行注冊,這樣才能創建該類型的窗口 return -1; HWND hwnd = CreateWindow( _T("ForTheDreamOfGameDevelop"),WINDOW_TITLE, //喜聞樂見的創建窗口函數CreateWindow WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, WINDOW_WIDTH, WINDOW_HEIGHT, NULL, NULL, hInstance, NULL ); //Direct3D資源的初始化,調用失敗用messagebox予以顯示 if (!(S_OK==Direct3D_Init (hwnd,hInstance))) { MessageBox(hwnd, _T("Direct3D初始化失敗~!"), _T("淺墨的消息窗口"), 0); //使用MessageBox函數,創建一個消息窗口 } PlaySound(L"GameMedia\\青鳥-《火影忍者》.wav", NULL, SND_FILENAME | SND_ASYNC|SND_LOOP); //循環播放背景音樂 MoveWindow(hwnd,200,10,WINDOW_WIDTH,WINDOW_HEIGHT,true); //調整窗口顯示時的位置,窗口左上角位于屏幕坐標(200,10)處 ShowWindow( hwnd, nShowCmd ); //調用Win32函數ShowWindow來顯示窗口 UpdateWindow(hwnd); //對窗口進行更新,就像我們買了新房子要裝修一樣 //進行DirectInput類的初始化 g_pDInput = new DInputClass(); g_pDInput->Init(hwnd,hInstance,DISCL_FOREGROUND | DISCL_NONEXCLUSIVE,DISCL_FOREGROUND | DISCL_NONEXCLUSIVE); //消息循環過程 MSG msg = { 0 }; //初始化msg while( msg.message != WM_QUIT ) //使用while循環 { static FLOAT fLastTime = (float)::timeGetTime(); static FLOAT fCurrTime = (float)::timeGetTime(); static FLOAT fTimeDelta = 0.0f; fCurrTime = (float)::timeGetTime(); fTimeDelta = (fCurrTime - fLastTime) / 1000.0f; fLastTime = fCurrTime; if( PeekMessage( &msg, 0, 0, 0, PM_REMOVE ) ) //查看應用程序消息隊列,有消息時將隊列中的消息派發出去。 { TranslateMessage( &msg ); //將虛擬鍵消息轉換為字符消息 DispatchMessage( &msg ); //該函數分發一個消息給窗口程序。 } else { Direct3D_Update(hwnd,fTimeDelta); //調用更新函數,進行畫面的更新 Direct3D_Render(hwnd,fTimeDelta); //調用渲染函數,進行畫面的渲染 } } UnregisterClass(_T("ForTheDreamOfGameDevelop"), wndClass.hInstance); return 0; } //-----------------------------------【WndProc( )函數】-------------------------------------- // 描述:窗口過程函數WndProc,對窗口消息進行處理 //------------------------------------------------------------------------------------------------ LRESULT CALLBACK WndProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam ) //窗口過程函數WndProc { switch( message ) //switch語句開始 { case WM_PAINT: // 客戶區重繪消息 Direct3D_Render(hwnd,0.0f); //調用Direct3D_Render函數,進行畫面的繪制 ValidateRect(hwnd, NULL); // 更新客戶區的顯示 break; //跳出該switch語句 case WM_KEYDOWN: // 鍵盤按下消息 if (wParam == VK_ESCAPE) // ESC鍵 DestroyWindow(hwnd); // 銷毀窗口, 并發送一條WM_DESTROY消息 break; case WM_DESTROY: //窗口銷毀消息 Direct3D_CleanUp(); //調用Direct3D_CleanUp函數,清理COM接口對象 PostQuitMessage( 0 ); //向系統表明有個線程有終止請求。用來響應WM_DESTROY消息 break; //跳出該switch語句 default: //若上述case條件都不符合,則執行該default語句 return DefWindowProc( hwnd, message, wParam, lParam ); //調用缺省的窗口過程來為應用程序沒有處理的窗口消息提供缺省的處理。 } return 0; //正常退出 } //-----------------------------------【Direct3D_Init( )函數】---------------------------------- // 描述:Direct3D初始化函數,進行Direct3D的初始化 //------------------------------------------------------------------------------------------------ HRESULT Direct3D_Init(HWND hwnd,HINSTANCE hInstance) { //-------------------------------------------------------------------------------------- // 【Direct3D初始化四步曲之一,創接口】:創建Direct3D接口對象, 以便用該Direct3D對象創建Direct3D設備對象 //-------------------------------------------------------------------------------------- LPDIRECT3D9 pD3D = NULL; //Direct3D接口對象的創建 if( NULL == ( pD3D = Direct3DCreate9( D3D_SDK_VERSION ) ) ) //初始化Direct3D接口對象,并進行DirectX版本協商 return E_FAIL; //-------------------------------------------------------------------------------------- // 【Direct3D初始化四步曲之二,取信息】:獲取硬件設備信息 //-------------------------------------------------------------------------------------- D3DCAPS9 caps; int vp = 0; if( FAILED( pD3D->GetDeviceCaps( D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, &caps ) ) ) { return E_FAIL; } if( caps.DevCaps & D3DDEVCAPS_HWTRANSFORMANDLIGHT ) vp = D3DCREATE_HARDWARE_VERTEXPROCESSING; //支持硬件頂點運算,我們就采用硬件頂點運算,妥妥的 else vp = D3DCREATE_SOFTWARE_VERTEXPROCESSING; //不支持硬件頂點運算,無奈只好采用軟件頂點運算 //-------------------------------------------------------------------------------------- // 【Direct3D初始化四步曲之三,填內容】:填充D3DPRESENT_PARAMETERS結構體 //-------------------------------------------------------------------------------------- D3DPRESENT_PARAMETERS d3dpp; ZeroMemory(&d3dpp, sizeof(d3dpp)); d3dpp.BackBufferWidth = WINDOW_WIDTH; d3dpp.BackBufferHeight = WINDOW_HEIGHT; d3dpp.BackBufferFormat = D3DFMT_A8R8G8B8; d3dpp.BackBufferCount = 2; d3dpp.MultiSampleType = D3DMULTISAMPLE_NONE; d3dpp.MultiSampleQuality = 0; d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD; d3dpp.hDeviceWindow = hwnd; d3dpp.Windowed = true; d3dpp.EnableAutoDepthStencil = true; d3dpp.AutoDepthStencilFormat = D3DFMT_D24S8; d3dpp.Flags = 0; d3dpp.FullScreen_RefreshRateInHz = 0; d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_IMMEDIATE; //-------------------------------------------------------------------------------------- // 【Direct3D初始化四步曲之四,創設備】:創建Direct3D設備接口 //-------------------------------------------------------------------------------------- if(FAILED(pD3D->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hwnd, vp, &d3dpp, &g_pd3dDevice))) return E_FAIL; //獲取顯卡信息到g_strAdapterName中,并在顯卡名稱之前加上“當前顯卡型號:”字符串 wchar_t TempName[60]=L"當前顯卡型號:"; //定義一個臨時字符串,且方便了把"當前顯卡型號:"字符串引入我們的目的字符串中 D3DADAPTER_IDENTIFIER9 Adapter; //定義一個D3DADAPTER_IDENTIFIER9結構體,用于存儲顯卡信息 pD3D->GetAdapterIdentifier(0,0,&Adapter);//調用GetAdapterIdentifier,獲取顯卡信息 int len = MultiByteToWideChar(CP_ACP,0, Adapter.Description, -1, NULL, 0);//顯卡名稱現在已經在Adapter.Description中了,但是其為char類型,我們要將其轉為wchar_t類型 MultiByteToWideChar(CP_ACP, 0, Adapter.Description, -1, g_strAdapterName, len);//這步操作完成后,g_strAdapterName中就為當前我們的顯卡類型名的wchar_t型字符串了 wcscat_s(TempName,g_strAdapterName);//把當前我們的顯卡名加到“當前顯卡型號:”字符串后面,結果存在TempName中 wcscpy_s(g_strAdapterName,TempName);//把TempName中的結果拷貝到全局變量g_strAdapterName中,大功告成~ if(!(S_OK==Objects_Init())) return E_FAIL; SAFE_RELEASE(pD3D) //LPDIRECT3D9接口對象的使命完成,我們將其釋放掉 return S_OK; } //-----------------------------------【Object_Init( )函數】-------------------------------------- // 描述:渲染資源初始化函數,在此函數中進行要被渲染的物體的資源的初始化 //-------------------------------------------------------------------------------------------------- HRESULT Objects_Init() { //-----------------------------【創建字體】--------------------------------------- D3DXCreateFont(g_pd3dDevice, 36, 0, 0, 1000, false, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, DEFAULT_QUALITY, 0, _T("Calibri"), &g_pTextFPS); D3DXCreateFont(g_pd3dDevice, 20, 0, 1000, 0, false, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, DEFAULT_QUALITY, 0, L"華文中宋", &g_pTextAdaperName); D3DXCreateFont(g_pd3dDevice, 23, 0, 1000, 0, false, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, DEFAULT_QUALITY, 0, L"微軟雅黑", &g_pTextHelper); D3DXCreateFont(g_pd3dDevice, 26, 0, 1000, 0, false, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, DEFAULT_QUALITY, 0, L"黑體", &g_pTextInfor); // -----------------------------【創建地面頂點緩存】-------------------------------- g_pd3dDevice->CreateVertexBuffer(4 * sizeof(CUSTOMVERTEX), 0, D3DFVF_CUSTOMVERTEX, D3DPOOL_MANAGED, &g_pFloorVBuffer, NULL); CUSTOMVERTEX *pVertices = NULL; g_pFloorVBuffer->Lock(0, 0, (void**)&pVertices, 0); pVertices[0] = CUSTOMVERTEX(-5000.0f, 0.0f, -5000.0f, 0.0f, 10.0f); pVertices[1] = CUSTOMVERTEX(-5000.0f, 0.0f, 5000.0f, 0.0f, 0.0f); pVertices[2] = CUSTOMVERTEX( 5000.0f, 0.0f, -5000.0f, 10.0f, 10.0f); pVertices[3] = CUSTOMVERTEX( 5000.0f, 0.0f, 5000.0f, 10.0f, 0.0f); g_pFloorVBuffer->Unlock(); // -----------------------------【創建NPC的頂點緩存】-------------------------------- g_pd3dDevice->CreateVertexBuffer( 4 * sizeof(CUSTOMVERTEX), 0, D3DFVF_CUSTOMVERTEX, D3DPOOL_MANAGED, &g_pNPCVBuffer , NULL ); pVertices = NULL; g_pNPCVBuffer ->Lock(0, 0, (void**)&pVertices, 0); pVertices[0] = CUSTOMVERTEX(-100.0f, 0.0f, 0.0f, 0.0f, 1.0f); pVertices[1] = CUSTOMVERTEX(-100.0f, 320.0f, 0.0f, 0.0f, 0.0f); pVertices[2] = CUSTOMVERTEX( 100.0f, 0.0f, 0.0f, 1.0f, 1.0f); pVertices[3] = CUSTOMVERTEX( 100.0f, 320.0f, 0.0f, 1.0f, 0.0f); g_pNPCVBuffer ->Unlock(); // -----------------------------【創建地面紋理】--------------------------------------- D3DXCreateTextureFromFile(g_pd3dDevice, L"GameMedia\\floor.jpg", &g_pFloorTexture); // -----------------------------【創建NPC紋理】--------------------------------------- D3DXCreateTextureFromFile( g_pd3dDevice, L"GameMedia\\girl.png", &g_pNPCTexture ); // -----------------------------【創建并初始化虛擬攝像機】--------------------------------------- g_pCamera = new CameraClass(g_pd3dDevice); g_pCamera->SetCameraPosition(&D3DXVECTOR3(0.0f, 500.0f, -800.0f)); //設置攝像機所在的位置 g_pCamera->SetTargetPosition(&D3DXVECTOR3(0.0f, 800.0f, 0.0f)); //設置目標觀察點所在的位置 g_pCamera->SetViewMatrix(); //設置取景變換矩陣 D3DXMATRIX matProj; D3DXMatrixPerspectiveFovLH(&matProj, D3DX_PI / 4.0f, 1.0f, 1.0f, 200000.0f); g_pCamera->SetProjMatrix(&matProj); //-----------------------------【創建并初始化天空對象】--------------------------------------- g_pSkyBox = new SkyBoxClass( g_pd3dDevice ); g_pSkyBox->LoadSkyTextureFromFile(L"GameMedia\\TropicalSunnyDayFront2048.png",L"GameMedia\\TropicalSunnyDayBack2048.png",L"GameMedia\\TropicalSunnyDayRight2048.png",L"GameMedia\\TropicalSunnyDayLeft2048.png", L"GameMedia\\TropicalSunnyDayUp2048.png");//從文件加載前、后、左、右、頂面5個面的紋理圖 g_pSkyBox->InitSkyBox(50000); //設置天空盒的邊長 //-----------------------------【創建并初始化雪花粒子系統】--------------------------------------- g_pSnowParticles = new SnowParticleClass(g_pd3dDevice); g_pSnowParticles->InitSnowParticle(); // 關閉光照 // g_pd3dDevice->SetRenderState( D3DRS_LIGHTING, false ); return S_OK; } //-----------------------------------【Direct3D_Update( )函數】-------------------------------- // 描述:不是即時渲染代碼但是需要即時調用的,如按鍵后的坐標的更改,都放在這里 //-------------------------------------------------------------------------------------------------- void Direct3D_Update( HWND hwnd,FLOAT fTimeDelta) { //使用DirectInput類讀取數據 g_pDInput->GetInput(); // 沿攝像機各分量移動視角 if (g_pDInput->IsKeyDown(DIK_A)) g_pCamera->MoveAlongRightVec(-1.0f); if (g_pDInput->IsKeyDown(DIK_D)) g_pCamera->MoveAlongRightVec( 1.0f); if (g_pDInput->IsKeyDown(DIK_W)) g_pCamera->MoveAlongLookVec( 1.0f); if (g_pDInput->IsKeyDown(DIK_S)) g_pCamera->MoveAlongLookVec(-1.0f); if (g_pDInput->IsKeyDown(DIK_R)) g_pCamera->MoveAlongUpVec( 1.0f); if (g_pDInput->IsKeyDown(DIK_F)) g_pCamera->MoveAlongUpVec(-1.0f); //沿攝像機各分量旋轉視角 if (g_pDInput->IsKeyDown(DIK_LEFT)) g_pCamera->RotationUpVec(-0.003f); if (g_pDInput->IsKeyDown(DIK_RIGHT)) g_pCamera->RotationUpVec( 0.003f); if (g_pDInput->IsKeyDown(DIK_UP)) g_pCamera->RotationRightVec(-0.003f); if (g_pDInput->IsKeyDown(DIK_DOWN)) g_pCamera->RotationRightVec( 0.003f); if (g_pDInput->IsKeyDown(DIK_Q)) g_pCamera->RotationLookVec(0.001f); if (g_pDInput->IsKeyDown(DIK_E)) g_pCamera->RotationLookVec( -0.001f); //鼠標控制右向量和上向量的旋轉 g_pCamera->RotationUpVec(g_pDInput->MouseDX()* 0.0003f); g_pCamera->RotationRightVec(g_pDInput->MouseDY() * 0.0003f); //鼠標滾輪控制觀察點收縮操作 static FLOAT fPosZ=0.0f; fPosZ += g_pDInput->MouseDZ()*0.03f; //計算并設置取景變換矩陣 D3DXMATRIX matView; g_pCamera->CalculateViewMatrix(&matView); g_pd3dDevice->SetTransform(D3DTS_VIEW, &matView); //把正確的世界變換矩陣存到g_matWorld中 D3DXMatrixTranslation(&g_matWorld, 0.0f, 0.0f, fPosZ); //以下這段代碼用于限制鼠標光標移動區域 POINT lt,rb; RECT rect; GetClientRect(hwnd,&rect); //取得窗口內部矩形 //將矩形左上點坐標存入lt中 lt.x = rect.left; lt.y = rect.top; //將矩形右下坐標存入rb中 rb.x = rect.right; rb.y = rect.bottom; //將lt和rb的窗口坐標轉換為屏幕坐標 ClientToScreen(hwnd,<); ClientToScreen(hwnd,&rb); //以屏幕坐標重新設定矩形區域 rect.left = lt.x; rect.top = lt.y; rect.right = rb.x; rect.bottom = rb.y; //限制鼠標光標移動區域 ClipCursor(&rect); ShowCursor(false); //隱藏鼠標光標 } //-----------------------------------【Direct3D_Render( )函數】------------------------------- // 描述:使用Direct3D進行渲染 //---------------------------------------------------------------------------------------------- void Direct3D_Render(HWND hwnd,FLOAT fTimeDelta) { //-------------------------------------------------------------------------------------- // 【Direct3D渲染五步曲之一】:清屏操作 //-------------------------------------------------------------------------------------- g_pd3dDevice->Clear(0, NULL, D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER|D3DCLEAR_STENCIL, D3DCOLOR_XRGB(100, 155, 255), 1.0f, 0); //-------------------------------------------------------------------------------------- // 【Direct3D渲染五步曲之二】:開始繪制 //-------------------------------------------------------------------------------------- g_pd3dDevice->BeginScene(); // 開始繪制 //-------------------------------------------------------------------------------------- // 【Direct3D渲染五步曲之三】:正式繪制 //-------------------------------------------------------------------------------------- //設置紋理狀態 g_pd3dDevice->SetTextureStageState( 0, D3DTSS_COLOROP, D3DTOP_SELECTARG1); g_pd3dDevice->SetTextureStageState( 0, D3DTSS_COLORARG1, D3DTA_TEXTURE ); g_pd3dDevice->SetSamplerState( 0, D3DSAMP_MINFILTER, D3DTEXF_LINEAR ); g_pd3dDevice->SetSamplerState( 0, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR ); //Alpha混合設置, 設置混合系數 g_pd3dDevice->SetRenderState( D3DRS_ALPHABLENDENABLE, true ); g_pd3dDevice->SetRenderState( D3DRS_SRCBLEND, D3DBLEND_SRCALPHA ); g_pd3dDevice->SetRenderState( D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA ); //-----------------------------【繪制地板】----------------------------- D3DXMATRIX matFloor; D3DXMatrixTranslation(&matFloor, 0.0f, 0.0f, 0.0f); g_pd3dDevice->SetTransform(D3DTS_WORLD, &matFloor); g_pd3dDevice->SetStreamSource(0, g_pFloorVBuffer, 0, sizeof(CUSTOMVERTEX)); g_pd3dDevice->SetFVF(D3DFVF_CUSTOMVERTEX); g_pd3dDevice->SetTexture(0, g_pFloorTexture); g_pd3dDevice->DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2); //-----------------------------【利用公告板技術準備繪制NPC】-------------------------- //取得當前的取景變換矩陣 D3DXMATRIX matView; g_pCamera->CalculateViewMatrix(&matView); //根據取景變換矩陣來計算并構造公告板矩陣 D3DXMATRIX matBillboard; D3DXMatrixIdentity(&matBillboard); matBillboard._11 = matView._11; matBillboard._13 = matView._13; matBillboard._31 = matView._31; matBillboard._33 = matView._33; D3DXMatrixInverse(&matBillboard, NULL, &matBillboard); //創建一個矩陣,右乘之前的公告板矩陣,并設置成世界矩陣 D3DXMATRIX matNPC; D3DXMatrixIdentity(&matNPC); matNPC = matBillboard * matNPC; g_pd3dDevice->SetTransform( D3DTS_WORLD, &matNPC ); //-----------------------------【正式繪制NPC人物】----------------------------- g_pd3dDevice->SetTexture( 0, g_pNPCTexture ); g_pd3dDevice->SetStreamSource( 0, g_pNPCVBuffer , 0, sizeof(CUSTOMVERTEX) ); g_pd3dDevice->SetFVF( D3DFVF_CUSTOMVERTEX ); g_pd3dDevice->DrawPrimitive( D3DPT_TRIANGLESTRIP, 0, 2 ); //-----------------------------【繪制天空】----------------------------- D3DXMATRIX matSky,matTransSky,matRotSky; D3DXMatrixTranslation(&matTransSky,0.0f,-15000.0f,0.0f); D3DXMatrixRotationY(&matRotSky, -0.00002f*timeGetTime()); //旋轉天空網格, 簡單模擬云彩運動效果 matSky=matTransSky*matRotSky; g_pSkyBox->RenderSkyBox(&matSky, false); //-----------------------------【繪制雪花粒子系統】------------------------ g_pSnowParticles->UpdateSnowParticle(fTimeDelta); g_pSnowParticles->RenderSnowParticle(); //-----------------------------【繪制文字信息】----------------------------- HelpText_Render(hwnd); //-------------------------------------------------------------------------------------- // 【Direct3D渲染五步曲之四】:結束繪制 //-------------------------------------------------------------------------------------- g_pd3dDevice->EndScene(); // 結束繪制 //-------------------------------------------------------------------------------------- // 【Direct3D渲染五步曲之五】:顯示翻轉 //-------------------------------------------------------------------------------------- g_pd3dDevice->Present(NULL, NULL, NULL, NULL); // 翻轉與顯示 } //-----------------------------------【HelpText_Render( )函數】------------------------------- // 描述:封裝了幫助信息的函數 //-------------------------------------------------------------------------------------------------- void HelpText_Render(HWND hwnd) { //定義一個矩形,用于獲取主窗口矩形 RECT formatRect; GetClientRect(hwnd, &formatRect); //在窗口右上角處,顯示每秒幀數 formatRect.top = 5; int charCount = swprintf_s(g_strFPS, 20, _T("FPS:%0.3f"), Get_FPS() ); g_pTextFPS->DrawText(NULL, g_strFPS, charCount , &formatRect, DT_TOP | DT_RIGHT, D3DCOLOR_RGBA(0,239,136,255)); //顯示顯卡類型名 g_pTextAdaperName->DrawText(NULL,g_strAdapterName, -1, &formatRect, DT_TOP | DT_LEFT, D3DXCOLOR(1.0f, 0.5f, 0.0f, 1.0f)); // 輸出幫助信息 formatRect.left = 0,formatRect.top = 380; g_pTextInfor->DrawText(NULL, L"控制說明:", -1, &formatRect, DT_SINGLELINE | DT_NOCLIP | DT_LEFT, D3DCOLOR_RGBA(235,123,230,255)); formatRect.top += 35; g_pTextHelper->DrawText(NULL, L" W:向前飛翔 S:向后飛翔 ", -1, &formatRect, DT_SINGLELINE | DT_NOCLIP | DT_LEFT, D3DCOLOR_RGBA(255,200,0,255)); formatRect.top += 25; g_pTextHelper->DrawText(NULL, L" A:向左飛翔 D:向右飛翔", -1, &formatRect, DT_SINGLELINE | DT_NOCLIP | DT_LEFT, D3DCOLOR_RGBA(255,200,0,255)); formatRect.top += 25; g_pTextHelper->DrawText(NULL, L" R:垂直向上飛翔 F:垂直向下飛翔", -1, &formatRect, DT_SINGLELINE | DT_NOCLIP | DT_LEFT, D3DCOLOR_RGBA(255,200,0,255)); formatRect.top += 25; g_pTextHelper->DrawText(NULL, L" Q:向左傾斜 E:向右傾斜", -1, &formatRect, DT_SINGLELINE | DT_NOCLIP | DT_LEFT, D3DCOLOR_RGBA(255,200,0,255)); formatRect.top += 25; g_pTextHelper->DrawText(NULL, L" 上、下、左、右方向鍵、鼠標移動:視角變化 ", -1, &formatRect, DT_SINGLELINE | DT_NOCLIP | DT_LEFT, D3DCOLOR_RGBA(255,200,0,255)); formatRect.top += 25; g_pTextHelper->DrawText(NULL, L" 鼠標滾輪:人物模型Y軸方向移動", -1, &formatRect, DT_SINGLELINE | DT_NOCLIP | DT_LEFT, D3DCOLOR_RGBA(255,200,0,255)); formatRect.top += 25; g_pTextHelper->DrawText(NULL, L" ESC鍵 : 退出程序", -1, &formatRect, DT_SINGLELINE | DT_NOCLIP | DT_LEFT, D3DCOLOR_RGBA(255,200,0,255)); } //-----------------------------------【Get_FPS( )函數】------------------------------------------ // 描述:用于計算每秒幀速率的一個函數 //-------------------------------------------------------------------------------------------------- float Get_FPS() { //定義四個靜態變量 static float fps = 0; //我們需要計算的FPS值 static int frameCount = 0;//幀數 static float currentTime =0.0f;//當前時間 static float lastTime = 0.0f;//持續時間 frameCount++;//每調用一次Get_FPS()函數,幀數自增1 currentTime = timeGetTime()*0.001f;//獲取系統時間,其中timeGetTime函數返回的是以毫秒為單位的系統時間,所以需要乘以0.001,得到單位為秒的時間 //如果當前時間減去持續時間大于了1秒鐘,就進行一次FPS的計算和持續時間的更新,并將幀數值清零 if(currentTime - lastTime > 1.0f) //將時間控制在1秒鐘 { fps = (float)frameCount /(currentTime - lastTime);//計算這1秒鐘的FPS值 lastTime = currentTime; //將當前時間currentTime賦給持續時間lastTime,作為下一秒的基準時間 frameCount = 0;//將本次幀數frameCount值清零 } return fps; } //-----------------------------------【Direct3D_CleanUp( )函數】-------------------------------- // 描述:對Direct3D的資源進行清理,釋放COM接口對象 //--------------------------------------------------------------------------------------------------- void Direct3D_CleanUp() { //釋放COM接口對象 SAFE_DELETE(g_pDInput); SAFE_RELEASE(g_pd3dDevice); SAFE_RELEASE(g_pTextAdaperName) SAFE_RELEASE(g_pTextHelper) SAFE_RELEASE(g_pTextInfor) SAFE_RELEASE(g_pTextFPS) SAFE_RELEASE(g_pd3dDevice) }
然后是幾張程序截圖:
我們可以發現,無論以水平方向上哪個角度去觀察這張圖片,都儼然顯現出3D模型的真實立體效果,這就是公告板技術的功勞,在這么真的3D效果面前,可不要又一次被一點雕蟲小技蒙蔽了雙眼哦。
文章最后,依舊是放出本篇文章配套源代碼的下載:
本篇文章配套源代碼請點擊這里下載:
【淺墨DirectX提高班】配套源代碼之二十一下載(CSDN下載頻道)
【淺墨DirectX提高班】配套源代碼之二十一下載(新浪微盤)
以上就是本節筆記的全部內容,更多精彩內容,且聽下回分解。
文章最后,依然是【每文一語】欄目,今天的句子是:
心是一個人的翅膀,心有多大,世界就有多大。很多時候限制我們的,不是周遭的環境,也不是他人的言行,而是我們自己。看不開、忘不了、放不下,把自己囚禁在灰暗的記憶里;不敢想、不自信、不行動,把自己局限在固定的空間里……如果不能打破心的禁錮,即使給你整個天空,你也找不到自由的感覺。加油:)
下周一,讓我們離游戲開發的夢想更近一步。
下周一,游戲開發筆記,我們,不見不散。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。