【Visual C++】游戏开发五十四 浅墨DirectX教程二十一 视觉的诡计 公告板 Billboard 技术

分享一下我老师大神的人工智能教程!零基础,通俗易懂!http://blog.csdn.net/jiangjunshow

也欢迎大家转载本篇文章。分享知识,造福人民,实现我们中华民族伟大复兴!

               


 本系列文章由zhmxy555(毛星云)编写,转载请注明出处。  

 文章链接: http://blog.csdn.net/poem_qianmo/article/details/13278851

 作者:毛星云(浅墨)    微博:@浅墨_毛星云      邮箱: 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 )   //窗口过程函数WndProcswitch( 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消息  breakcase 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 = 0if( 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.0f0.0f, 10.0f); pVertices[1] = CUSTOMVERTEX(-5000.0f, 0.0f5000.0f0.0f0.0f); pVertices[2] = CUSTOMVERTEX( 5000.0f, 0.0f, -5000.0f, 10.0f, 10.0f);  pVertices[3] = CUSTOMVERTEX( 5000.0f, 0.0f5000.0f, 10.0f0.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 = 5int 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提高班】配套源代码之二十一下载 (新浪微盘)



以上就是本节笔记的全部内容,更多精彩内容,且听下回分解。




文章最后,依然是【每文一语】栏目,今天的句子是: 



心是一个人的翅膀,心有多大,世界就有多大。很多时候限制我们的,不是周遭的环境,也不是他人的言行,而是我们自己。看不开、忘不了、放不下,把自己囚禁在灰暗的记忆里;不敢想、不自信、不行动,把自己局限在固定的空间里……如果不能打破心的禁锢,即使给你整个天空,你也找不到自由的感觉。加油:)




下周一,让我们离游戏开发的梦想更近一步。

下周一,游戏开发笔记,我们,不见不散。



           

给我老师的人工智能教程打call!http://blog.csdn.net/jiangjunshow
这里写图片描述
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值