【Visual C++】游戏开发五十五 浅墨DirectX教程二十二 水乳交融的美学 alpha混合技术

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

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

               



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

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

 作者:毛星云(浅墨)    邮箱: happylifemxy@163.com  


在这篇文章里面,我们一起非常详细地探讨了Direct3D中Alpha混合相关的内容。首先是认识了Alpha通道与混合技术,然后结识了融合因子,了解了融合运算方式和融合因子的取法,以及Alpha的三处来源,接着是大家喜闻乐见的极易上手的使用三部曲,最后依旧是详细注释的程序源码的欣赏,程序截图和每文一语栏目。

 

放截图吧,不过为了不毁三观,我们还是先放原版带纹理的截图:



对比图,我们今天为了演示做出来的alpha效果图:



说实话,这样的人物模型用来做Alpha混合的演示有些凶残,但是为了更好的掌握游戏编程,我们豁出去了。:)




 然后依旧是先和大家聊聊天,

昨天晚上去看了周杰伦摩天轮演唱会南京站,表示现场真是太震撼了。当杰伦唱起那些老歌比如《回到过去》,《晴天》的时候,让浅墨想起了初中时代的那些无忧无虑的日子,发现时间真的是一去不复返了。

另外就是南京最近天气忽然转冷,浅墨一不小心中招感冒了,希望大家一定要注意防寒哈。

然后今天就是双十一抢购日了,希望大家都能抢到自己心仪的宝贝。

好了,废话不多说,我们进入正题吧。

 

 

 


一、初识Alpha通道与混合技术

 

大家应该都知道, Alpha通道是计算机中存储一张图片的透明和半透明度信息的通道。它是一个8位的灰度通道,用256级灰度来记录图像中的透明度信息,定义透明、不透明和半透明区域,其中黑表示全透明,白表示不透明,灰表示半透明。

而混合,为Blending的英译,是计算机图形学中常用的一种技术,即混合像素。我们通常用已经光栅化的像素光栅化同一位置的像素,或者说是在某图元上混合图元。这样说不好理解的话,我们来举个例子。说白了就是在一张图元的地盘上又来了另一张图元,然后他们按照我们指定的某种方式来像“揉面团”一样揉在一起,进行加工了再在原地显示出来。

 

 alpha混合技术对熟悉游戏的朋友们来说应该不会陌生,这种技术在如今的游戏特效里已经被用烂了。且不说3D游戏中它的频繁登场,就算是2D的游戏中,这种技术也是满眼皆是。

  alpha混合听上去很神秘,实际非常简单,其作用就是要实现一种半透明效果。假设一种不透明东西的颜色是A,另一种透明的东西的颜色是B,那么透过B去看A,看上去的颜色C就是B和A的混合颜色,可以用这个式子来近似,设B物体的透明度为alpha(取值为0-1,0为完全透明,1为完全不透明)

 

 R(C)=alpha*R(B)+(1-alpha)*R(A)

 G(C)=alpha*G(B)+(1-alpha)*G(A)

 B(C)=alpha*B(B)+(1-alpha)*B(A)

 

  其中R(x)、G(x)、B(x)分别指颜色x的RGB分量(这里自变量x取的是颜色C)。看起来这个东西这么简单,可是用它实现的效果确实非常的华丽,应用alpha混合技术,可以实现出最眩目的火光、烟雾、阴影、动态光源等等一切我们可以想象的出来的半透明效果。

 

 



二、Direct3D中的融合套路——融合因子


上面我们讲到的是一般意义上的混合技术,而在Direct3D中,我们应该按如下的思路来进一步理解。

首先,Direct3D中依然是用Alpha通道来实现多个像素颜色值的融合。每个像素都包含四个分量:Alpha分量、红色分量、绿色分量和蓝色分量(即ARGB四分量)。其中,Alpha分量用于指定像素的透明度,在0~255之间取值,0表示完全透明,255表示完全不透明。另外,根据使用的不同的颜色宏的区别,还可能是在0.0~1.0之间取值。

 

在Direct3D中,融合这一领域有一个权威,那便是Alpha融合公式。Alpha融合公式如下:

     

其中RGB_src 和RGB_dst分别表示源像素和目标像素的颜色值,为包含四个颜色分量的颜色值。

K_src 和K_dst分别表示源融合因子和目标融合因子。他们指定了源像素和目标像素的颜色值在融合过程中所占的比例,在[0,1]之间取值。通过原融合因子和目标融合因子,我们能够以多种方式来修改源像素和目标像素的颜色值,从而获得我们满意的最终的融合后的颜色值。稍后会讲解融合因子的具体取法,这里我们先把这个融合公式解析完。

在融合公式中,OP表示源和目标的融合运算方式,由D3DBLENDOP枚举体来指定,需要注意的是它的默认值是源计算结果和目标计算结果相加。而运算符“∙”表示颜色值的每个分量都与和相乘。




三、融合运算方式的取法


上面我们刚提到过,融合运算方式由D3DBLENDOP枚举体来指定。

我们指的是SetRenderState中的第二个参数在D3DBLENDOP枚举体中取值,而第一个参数,取D3DRS_BLENDOP。

这一节就来看一下这个D3DBLENDOP枚举体的定义:

typedef enum D3DBLENDOP {  D3DBLENDOP_ADD           = 1,  D3DBLENDOP_SUBTRACT      = 2,  D3DBLENDOP_REVSUBTRACT   = 3,  D3DBLENDOP_MIN           = 4,  D3DBLENDOP_MAX           = 5,  D3DBLENDOP_FORCE_DWORD   = 0x7fffffff } D3DBLENDOP, *LPD3DBLENDOP;


我们用一个列表来进行讲解吧:

  

D3DBLENDOP操作符

精析

D3DBLENDOP_ADD

源像素计算结果与目标像素的计算结果相加,即【最终结果】=【源】+【目标】

D3DBLENDOP_SUBTRACT

源像素计算结果与目标像素的计算结果相减,即【最终结果】=【源】-【目标】

D3DBLENDOP_REVSUBTRACT

目标像素的计算结果减去源像素计算结果,即【最终结果】=【目标】-【源】

D3DBLENDOP_MIN

在源像素计算结果和目标像素计算结果之间取小者。即【最终结果】= MIN(【目标】,【源】)

D3DBLENDOP_MAX

在源像素计算结果和目标像素计算结果之间取大者。即【最终结果】= MAX(【目标】,【源】)



 

我们需要取什么类型的融合运算方式,在表中查阅即可。再提醒大家一次,Direct3D中为我们默认取了融合运算方式为D3DBLENDOP_ADD,即源像素计算结果与目标像素的计算结果相加。




  四、融合因子的取法

 

接着我们来看一下融合因子和的取法。源融合因子和目标融合因子可以在SetRenderState方法中第一个参数取D3DRS_SRCBLEND和D3DRS_DESTBLEND分别进行设置,而第二个参数都是在一个D3DBLEND枚举体中进行的取值,我们在MSDN中查到它的原型如下:

typedef enum D3DBLEND {  D3DBLEND_ZERO              = 1,  D3DBLEND_ONE               = 2,  D3DBLEND_SRCCOLOR          = 3,  D3DBLEND_INVSRCCOLOR       = 4,  D3DBLEND_SRCALPHA          = 5,  D3DBLEND_INVSRCALPHA       = 6,  D3DBLEND_DESTALPHA         = 7,  D3DBLEND_INVDESTALPHA      = 8,  D3DBLEND_DESTCOLOR         = 9,  D3DBLEND_INVDESTCOLOR      = 10,  D3DBLEND_SRCALPHASAT       = 11,  D3DBLEND_BOTHSRCALPHA      = 12,  D3DBLEND_BOTHINVSRCALPHA   = 13,  D3DBLEND_BLENDFACTOR       = 14,  D3DBLEND_INVBLENDFACTOR    = 15,  D3DBLEND_SRCCOLOR2         = 16,  D3DBLEND_INVSRCCOLOR2      = 17,  D3DBLEND_FORCE_DWORD       = 0x7fffffff } D3DBLEND, *LPD3DBLEND;



依旧是通过一个表格来对其中常用的参数进行讲解,:

                       

D3DBLEND融合类型

精析

D3DBLEND_ZERO

融合因子=(0,0,0,0)

D3DBLEND_ONE

融合因子=(1,1,1,1)

D3DBLEND_SRCCOLOR

融合因子=(R_src,G_src,B_src,A_src)

D3DBLEND_INVSRCCOLOR

融合因子=(1-R_src,1-G_src,1-B_src,1-A_src)

D3DBLEND_SRCALPHA

融合因子=(1-A_src,A_src,A_src,A_src)

D3DBLEND_INVSRCALPHA

融合因子=(1-A_src,1-A_src,1-A_src,1-A_src)

D3DBLEND_DESTALPHA

融合因子=(A_dst , A_dst, A_dst  , A_dst)

D3DBLEND_INVDESTALPHA

融合因子= (1-A_dst, 1-A_dst, 1-A_dst , 1-A_dst ).

D3DBLEND_DESTCOLOR

融合因子=(R_dst , G_dst, B_dst  , A_dst).

D3DBLEND_INVDESTCOLOR

融合因子= (1 - R_dst, 1 - G_dst, 1 - B_dst, 1 - A_dst).

D3DBLEND_SRCALPHASAT

融合因子= (f, f, f, 1),其中f = min(A_src,1 - A_dst)


在上表中,R_src  , G_src , B_src  , A_src分别表示源(即source)像素的红、绿、蓝、透明四个分量值,而R_dst  , G_dst, B_dst  , A_dst表示目标(即destination)像素的红、绿、蓝、透明四个分量值。

大家需要什么类型的融合因子,在上表中进行查阅就行了。

 

       




五、Alpha的三处来源


我们在使用Alpha融合之前,还需要明确源像素和目标像素颜色值的Alpha分量来自何方。像素的Alpha值一般有三处来源,分别是顶点颜色的Alpha值、材质的Alpha值、纹理的Alpha值。我们通常在这三处来源中取一处就可以了。它们的优先级是这样的,纹理>材质>顶点颜色。

即这样理解:

首先我们看有没有使用纹理贴图。如果是使用了纹理贴图,那么像素的Alpha值就优先来源于纹理贴图的Alpha通道。

我是旁白:纹理的Alpha分量是高富帅,首先必须满足他。

再看有没有使用光照和材质。如果使用了材质,那么像素的Alpha值就优先来源于物体表面的材质。

我是旁白:在纹理的Alpha分量不在场的情况下,备胎“材质的Alpha分量”终于逆袭了。

最后若既没有使用纹理贴图也没有使用光照和材质,那么像素的Alpha值就只能来源于顶点的颜色值的Alpha分量了。

我是旁白逐梦便利贴:在纹理的Alpha分量和材质的Alpha分量都不在场的情况下,备胎的备胎“顶点的Alpha分量”也有春天啊。

 

 

下面我们分别看看这三处来源如何通过代码来指定。

 

1.顶点Alpha分量


首先我们要知道,顶点Alpha分量只是备胎的备胎而已,它在没有使用光照和材质的情况下才有上场的机会。

如果在程序中直接指定每个顶点的颜色,那么可以直接给出每个顶点颜色的Alpha值,并且这些顶点的Alpha值是可以在程序运行过程中动态修改的。

我们可以通过IDirect3DDevice9::SetTextureStageState方法指定Alpha值的来源,把第三个参数指定为D3DTA_DIFFUSE,来指定Alpha值来自顶点颜色。

 

在MSDN中查到这个函数原型如下:

   HRESULT SetTextureStageState(      [in] DWORD Stage,      [in]D3DTEXTURESTAGESTATETYPE Type,      [in] DWORD Value     );


■第一个参数,DWORD类型的Stage,指定当前设置的纹理层为第几层(有效值0~7)

■第二个参数,D3DTEXTURESTAGESTATETYPE类型的Type,填将要设置的纹理渲染状态,在枚举类型D3DTEXTURESTAGESTATETYPE中任意取值。

■第三个参数,DWORD类型的Value,表示所设置的状态值,它是根据第二个参数来决定具体取什么值的。

 

对于顶点Alpha分量,我们就这样来两句:

//计算漫反射颜色的alpha值g_pd3dDevice->SetTextureStageState(0,D3DTSS_ALPHAARG1, D3DTA_DIFFUSE);g_pd3dDevice->SetTextureStageState(0,D3DTSS_ALPHAOP, D3DTOP_SELECTARG1);


2.材质的Alpha分量

之前部分我们讲的材质的Alpha分量,充其量也只是个备胎而已,在场景内的物体没有指定纹理的时候,才有用武之地。在这种情况下,顶点的Alpha值取决于材质属性中漫反射颜色的Alpha系数以及灯光颜色中的Alpha系数,通过材质和光照中的Alpha系数相互作用,计算得到。我们知道,顶点的光照计算过程是分别针对红、绿、蓝、Alpha这四个颜色分量分开独立计算的。而我们关注的顶点的Alpha值就决定于光照计算结果的Alpha分量,和其他的红、绿、蓝三个分量毫无瓜葛。

比如我们可以这样来设定某材质的Alpha分量值,这句代码中我们把这种材质的漫反射颜色值的Alpha分量设为了0.2(范围为0.0~1.0)

g_pMaterial].Diffuse.a= 0.2f;


3.纹理的Alpha分量

作为不可一世的“高富帅”——纹理,既然它在物体表面上使用了,就必须首先满足它的要求,那么,像素的Alpha值就是纹理Alpha混合之后的值了。

所以这时候混合后的像素就取决于纹理的Alpha混合方式。而纹理Alpha混合方式决定了纹理Alpha混合之后的Alpha值是取自材质,还是取自纹理,抑或是取自这两者的某种运算。

若是取自纹理,我们就这样写:

m_pd3dDevice->SetTextureStageState( 0,D3DTSS_ALPHAARG1, D3DTA_TEXTURE );// Alpha值是取自材质 m_pd3dDevice->SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_SELECTARG1); //将纹理颜色混合的第一个参数的ALPHA值用于输出

我是旁白:可以使用DirectX SDK中提供的DirectX Texture Tool来为我们的素材纹理图片创建Alpha通道

 

 


六、Alpha融合使用三步曲


前面讲了那么多,现在依然是落实到一个字“用”上,依旧是为大家总结了一个使用三步曲,方便大家立竿见影,快速掌握Alpha融合技术的使用。


1. 三步曲之一:启用Alpha融合

在Direct3D中,混合默认是被关闭着的,要启用Alpha融合的话,我们就通过设置D3DRS_ALPHABLENDENABLE渲染状态为true,即写上这句代码:

m_pd3dDevice->SetRenderState(D3DRS_ALPHABLENDENABLE, true);


2. 三步曲之二:设置融合因子

启用了Alpha融合,第二步便是设置源融合因子和目标融合因子。前面我们已经详细讲解过源融合因子和目标融合因子的取值。源融合因子和目标融合因子分别可以在SetRenderState方法中第一个参数取D3DRS_SRCBLEND和D3DRS_DESTBLEND分别进行设置,第二个参数都是在一个D3DBLEND枚举体中进行的取值。

比如这样写,就是源融合因子=(A_src , A_src , A_src , A_src),目标融合因子=(1-A_src , 1-A_src , 1-A_src , 1-A_src),这是我们最常用的Alpha混合因子取法:

//设置融合因子  g_pd3dDevice->SetRenderState(D3DRS_SRCBLEND,D3DBLEND_SRCALPHA);    g_pd3dDevice->SetRenderState(D3DRS_DESTBLEND,D3DBLEND_INVSRCALPHA);

3. 三步曲之三:设置Alpha融合运算方式

因为Direct3D中有默认的融合运算方式D3DBLENDOP_ADD,即源像素计算结果与目标像素的计算结果相加。所以这一步其实可以省略,但是如果不想用这种融合运算方式,我们可以加上这一步,用SetRenderState方法来改成我们需要的运算方式,比如如下的代码,便将融合运算方式改成了D3DBLENDOP_SUBTRACT,即源像素计算结果与目标像素的计算结果相减:

g_pd3dDevice->SetRenderState(D3DRS_BLENDOP, D3DBLENDOP_SUBTRACT);


4. 以代码为载体理解

依旧是把这三步曲整合一下,在Direct3D中运用Alpha混合技术,其实最少只用写3句代码,第三步通常可以省略,即如下的代码:

 // 三步曲之一,开启Alpha融合 g_pd3dDevice->SetRenderState(D3DRS_ALPHABLENDENABLE, true); //三步曲之二,设置融合因子 g_pd3dDevice->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA); g_pd3dDevice->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA); //三步曲之三,设置融合运算方式 g_pd3dDevice->SetRenderState(D3DRS_BLENDOP, D3DBLENDOP_ADD);  //这句设置运算方式为D3DBLENDOP_ADD的代码Direct3D默认为我们写了,所以注释掉这句也没大碍




七、详细注释的源代码欣赏



我们今天的这个示例程序其实更准确的是接在学完3D游戏人物载入那一篇文章的示例程序之后的。这里我们没有载入模型的纹理,于是可以看到材质Alpha的效果。在Object_Init( )函数中载入材质时我们将代码经过了一些调整,才能达到预先想要的效果。好了,上主要的main函数代码吧:

//-----------------------------------【程序说明】----------------------------------------------//  【Visual C++】游戏开发系列配套源码五十五  浅墨DirectX教程二十二  水乳交融的美学:alpha混合技术  //  VS2010版//  2013年10月 Create by 浅墨//  背景音乐素材出处:Fort Minor-Where'd You Go//------------------------------------------------------------------------------------------------//-----------------------------------【宏定义部分】--------------------------------------------// 描述:定义一些辅助宏//------------------------------------------------------------------------------------------------#define SCREEN_WIDTH 932      //为窗口宽度定义的宏,以方便在此处修改窗口宽度#define SCREEN_HEIGHT 700       //为窗口高度定义的宏,以方便在此处修改窗口高度#define WINDOW_TITLE _T("【致我们永不熄灭的游戏开发梦想】  浅墨DirectX教程二十二  水乳交融的美学:alpha混合技术 博文配套示例程序 by浅墨") //为窗口标题定义的宏//-----------------------------------【头文件包含部分】---------------------------------------// 描述:包含程序所依赖的头文件//------------------------------------------------------------------------------------------------                                                                                      #include <d3d9.h>#include <d3dx9.h>#include <tchar.h>#include <time.h> #include "DirectInputClass.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") //-----------------------------------【全局变量声明部分】-------------------------------------// 描述:全局变量的声明//------------------------------------------------------------------------------------------------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类的指针LPD3DXMESH    g_pMesh     = NULL; // 网格对象D3DMATERIAL9*   g_pMaterials    = NULL; // 网格的材质信息LPDIRECT3DTEXTURE9*  g_pTextures     = NULL; // 网格的纹理信息DWORD      g_dwNumMtrls    = 0;    // 材质的数目//-----------------------------------【全局函数声明部分】-------------------------------------// 描述:全局函数声明,防止“未声明的标识”系列错误//------------------------------------------------------------------------------------------------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);void        Direct3D_Update( HWND hwnd);void        Direct3D_CleanUp( );float        Get_FPS();void        Matrix_Set();//-----------------------------------【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("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, SCREEN_WIDTH,  SCREEN_HEIGHT, NULL, NULL, hInstance, NULL ); //Direct3D资源的初始化,调用失败用messagebox予以显示 if (!(S_OK==Direct3D_Init (hwnd,hInstance))) {  MessageBox(hwnd, _T("Direct3D初始化失败~!"), _T("浅墨的消息窗口"), 0); //使用MessageBox函数,创建一个消息窗口  } PlaySound(L"Fort Minor-Where'd You Go.wav", NULL, SND_FILENAME | SND_ASYNC|SND_LOOP);   //循环播放背景音乐 MoveWindow(hwnd,200,50,SCREEN_WIDTH,SCREEN_HEIGHT,true);   //调整窗口显示时的位置,窗口左上角位于屏幕坐标(200,50)处 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循环 {  if( PeekMessage( &msg, 0, 0, 0, PM_REMOVE ) )   //查看应用程序消息队列,有消息时将队列中的消息派发出去。  {   TranslateMessage( &msg );  //将虚拟键消息转换为字符消息   DispatchMessage( &msg );  //该函数分发一个消息给窗口程序。  }  else  {   Direct3D_Update(hwnd);         //调用更新函数,进行画面的更新   Direct3D_Render(hwnd);   //调用渲染函数,进行画面的渲染     } } 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);          //调用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            = SCREEN_WIDTH; d3dpp.BackBufferHeight           = SCREEN_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);  // 从X文件中加载网格数据 LPD3DXBUFFER pAdjBuffer  = NULL; LPD3DXBUFFER pMtrlBuffer = NULL; D3DXLoadMeshFromX(L"69.X", D3DXMESH_MANAGED, g_pd3dDevice,   &pAdjBuffer, &pMtrlBuffer, NULL, &g_dwNumMtrls, &g_pMesh); // 读取材质和纹理数据 D3DXMATERIAL *pMtrls = (D3DXMATERIAL*)pMtrlBuffer->GetBufferPointer(); //创建一个D3DXMATERIAL结构体用于读取材质和纹理信息 g_pMaterials = new D3DMATERIAL9[g_dwNumMtrls]; g_pTextures  = new LPDIRECT3DTEXTURE9[g_dwNumMtrls]; for (DWORD i=0; i<g_dwNumMtrls; i++)  {  //获取材质,并设置一下环境光的颜色值  g_pMaterials[i] = pMtrls[i].MatD3D;  //g_pMaterials[i].Ambient = g_pMaterials[i].Diffuse;  g_pMaterials[i].Diffuse.a = 0.3f;//设置材质的Alpha分量  //创建一下纹理对象  g_pTextures[i]  = NULL;  //注释掉纹理的载入,让“备胎”材质的Alpha通道有上场的机会  //D3DXCreateTextureFromFileA(g_pd3dDevice, pMtrls[i].pTextureFilename, &g_pTextures[i]); } SAFE_RELEASE(pAdjBuffer) SAFE_RELEASE(pMtrlBuffer) // 设置光照 D3DLIGHT9 light; ::ZeroMemory(&light, sizeof(light)); light.Type          = D3DLIGHT_DIRECTIONAL; light.Ambient       = D3DXCOLOR(0.5f, 0.5f, 0.5f, 1.0f); light.Diffuse       = D3DXCOLOR(1.0f, 1.0f, 1.0f, 1.0f); light.Specular      = D3DXCOLOR(0.0f, 0.0f, 0.0f, 1.0f); light.Direction     = D3DXVECTOR3(1.0f, 0.0f, 1.0f); g_pd3dDevice->SetLight(0, &light); g_pd3dDevice->LightEnable(0, true); g_pd3dDevice->SetRenderState(D3DRS_NORMALIZENORMALS, true); g_pd3dDevice->SetRenderState(D3DRS_SPECULARENABLE, true); // 设置渲染状态 g_pd3dDevice->SetRenderState(D3DRS_CULLMODE, D3DCULL_CCW);   //开启背面消隐 g_pd3dDevice->SetRenderState(D3DRS_ZFUNC, D3DCMP_LESS);   //将深度测试函数设为D3DCMP_LESS g_pd3dDevice->SetRenderState(D3DRS_ZWRITEENABLE, true);     //深度测试成功后,更新深度缓存 // 三步曲之一,开启Alpha融合 g_pd3dDevice->SetRenderState(D3DRS_ALPHABLENDENABLE, true); //三步曲之二,设置融合因子 g_pd3dDevice->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA); g_pd3dDevice->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA); //三步曲之三,设置融合运算方式 g_pd3dDevice->SetRenderState(D3DRS_BLENDOP, D3DBLENDOP_ADD);  //这句设置运算方式为D3DBLENDOP_ADD的代码Direct3D默认为我们写了,所以注释掉这句也没大碍 return S_OK;}//-----------------------------------【Matrix_Set( )函数】--------------------------------------// 描述:封装了Direct3D四大变换的函数,即世界变换,取景变换,投影变换,视口变换的设置//--------------------------------------------------------------------------------------------------void Matrix_Set()//-------------------------------------------------------------------------------------- //【四大变换之二】:取景变换矩阵的设置 //-------------------------------------------------------------------------------------- D3DXMATRIX matView; //定义一个矩阵 D3DXVECTOR3 vEye(0.0f, 100.0f, -220.0f)//摄像机的位置 D3DXVECTOR3 vAt(0.0f, 40.0f, 0.0f); //观察点的位置 D3DXVECTOR3 vUp(0.0f, 1.0f, 0.0f);//向上的向量 D3DXMatrixLookAtLH(&matView, &vEye, &vAt, &vUp); //计算出取景变换矩阵 g_pd3dDevice->SetTransform(D3DTS_VIEW, &matView); //应用取景变换矩阵 //-------------------------------------------------------------------------------------- //【四大变换之三】:投影变换矩阵的设置 //-------------------------------------------------------------------------------------- D3DXMATRIX matProj; //定义一个矩阵 D3DXMatrixPerspectiveFovLH(&matProj, D3DX_PI / 4.0f,(float)((double)SCREEN_WIDTH/SCREEN_HEIGHT),1.0f, 1000.0f); //计算投影变换矩阵 g_pd3dDevice->SetTransform(D3DTS_PROJECTION, &matProj);  //设置投影变换矩阵 //-------------------------------------------------------------------------------------- //【四大变换之四】:视口变换的设置 //-------------------------------------------------------------------------------------- D3DVIEWPORT9 vp; //实例化一个D3DVIEWPORT9结构体,然后做填空题给各个参数赋值就可以了 vp.X      = 0;  //表示视口相对于窗口的X坐标 vp.Y      = 0;  //视口相对对窗口的Y坐标 vp.Width  = SCREEN_WIDTH; //视口的宽度 vp.Height = SCREEN_HEIGHT; //视口的高度 vp.MinZ   = 0.0f; //视口在深度缓存中的最小深度值 vp.MaxZ   = 1.0f//视口在深度缓存中的最大深度值 g_pd3dDevice->SetViewport(&vp); //视口的设置}//-----------------------------------【Direct3D_Update( )函数】--------------------------------// 描述:不是即时渲染代码但是需要即时调用的,如按键后的坐标的更改,都放在这里//--------------------------------------------------------------------------------------------------void  Direct3D_Update( HWND hwnd)//使用DirectInput类读取数据 g_pDInput->GetInput();//通过按键的按下来控制漫反射Alpha分量值的变化 if (g_pDInput->IsKeyDown(DIK_1))  //按下1键 {  for (DWORD i=0; i<g_dwNumMtrls; i++)   {   g_pMaterials[i].Diffuse.a+= 0.001f;  } } if (g_pDInput->IsKeyDown(DIK_2)) //按下1键 {  for (DWORD i=0; i<g_dwNumMtrls; i++)   {   g_pMaterials[i].Diffuse.a-= 0.001f;  } } // 按住鼠标左键并拖动,为平移操作 static FLOAT fPosX = 0.0f, fPosY = -50.0f, fPosZ = 0.0f;  if (g_pDInput->IsMouseButtonDown(0))  {  fPosX += (g_pDInput->MouseDX())*  0.08f;  fPosY += (g_pDInput->MouseDY()) * -0.08f; } //鼠标滚轮,为观察点收缩操作 fPosZ += (g_pDInput->MouseDZ())* 0.02f// 平移物体 if (g_pDInput->IsKeyDown(DIK_A)) fPosX -= 0.005fif (g_pDInput->IsKeyDown(DIK_D)) fPosX += 0.005fif (g_pDInput->IsKeyDown(DIK_W)) fPosY += 0.005fif (g_pDInput->IsKeyDown(DIK_S)) fPosY -= 0.005f; D3DXMatrixTranslation(&g_matWorld, fPosX, fPosY, fPosZ); // 按住鼠标右键并拖动,为旋转操作 static float fAngleX = 0, fAngleY =0;  if (g_pDInput->IsMouseButtonDown(1))  {  fAngleX += (g_pDInput->MouseDY())* -0.01f;  fAngleY += (g_pDInput->MouseDX()) * -0.01f; } // 旋转物体  if (g_pDInput->IsKeyDown(DIK_UP)) fAngleX += 0.005fif (g_pDInput->IsKeyDown(DIK_DOWN)) fAngleX -= 0.005f;  if (g_pDInput->IsKeyDown(DIK_LEFT)) fAngleY -= 0.005f;  if (g_pDInput->IsKeyDown(DIK_RIGHT)) fAngleY += 0.005f; D3DXMATRIX Rx, Ry; D3DXMatrixRotationX(&Rx, fAngleX); D3DXMatrixRotationY(&Ry, fAngleY); g_matWorld = Rx * Ry * g_matWorld; Matrix_Set();}//-----------------------------------【Direct3D_Render( )函数】-------------------------------// 描述:使用Direct3D进行渲染//--------------------------------------------------------------------------------------------------void Direct3D_Render(HWND hwnd)//-------------------------------------------------------------------------------------- // 【Direct3D渲染五步曲之一】:清屏操作 //-------------------------------------------------------------------------------------- g_pd3dDevice->Clear(0, NULL, D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER, D3DCOLOR_XRGB(160, 150, 150), 1.0f, 0); //定义一个矩形,用于获取主窗口矩形 RECT formatRect; GetClientRect(hwnd, &formatRect); //-------------------------------------------------------------------------------------- // 【Direct3D渲染五步曲之二】:开始绘制 //-------------------------------------------------------------------------------------- g_pd3dDevice->BeginScene();                     // 开始绘制  //-------------------------------------------------------------------------------------- // 【Direct3D渲染五步曲之三】:正式绘制 //-------------------------------------------------------------------------------------- //绘制3D模型 g_pd3dDevice->SetTransform(D3DTS_WORLD, &g_matWorld);//设置模型的世界矩阵,为绘制做准备 // 用一个for循环,进行模型的网格各个部分的绘制 for (DWORD i = 0; i < g_dwNumMtrls; i++) {  g_pd3dDevice->SetMaterial(&g_pMaterials[i]);  //设置此部分的材质  g_pd3dDevice->SetTexture(0, g_pTextures[i]);//设置此部分的纹理  g_pMesh->DrawSubset(i);  //绘制此部分 }   //在窗口右上角处,显示每秒帧数   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.top = 30;   static wchar_t strInfo[256] = {0};   swprintf_s(strInfo,-1, L"模型坐标: (%.2f, %.2f, %.2f)", g_matWorld._41, g_matWorld._42, g_matWorld._43);   g_pTextHelper->DrawText(NULL, strInfo, -1, &formatRect, DT_SINGLELINE | DT_NOCLIP | DT_LEFT, D3DCOLOR_RGBA(135,239,136,255));   // 输出帮助信息   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"    数字键1与2:增大或者缩小材质的Alpha值", -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"    按住鼠标右键并拖动:旋转模型", -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"    W、S、A、D键:平移模型 ", -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"    ESC键 : 退出程序", -1, &formatRect,     DT_SINGLELINE | DT_NOCLIP | DT_LEFT, D3DCOLOR_RGBA(255,200,0,255)); //-------------------------------------------------------------------------------------- // 【Direct3D渲染五步曲之四】:结束绘制 //-------------------------------------------------------------------------------------- g_pd3dDevice->EndScene();                       // 结束绘制 //-------------------------------------------------------------------------------------- // 【Direct3D渲染五步曲之五】:显示翻转 //-------------------------------------------------------------------------------------- g_pd3dDevice->Present(NULL, NULL, NULL, NULL);  // 翻转与显示  }//-----------------------------------【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接口对象 for (DWORD i = 0; i<g_dwNumMtrls; i++)   SAFE_RELEASE(g_pTextures[i]); SAFE_DELETE(g_pTextures);  SAFE_DELETE(g_pMaterials);  SAFE_DELETE(g_pDInput); SAFE_RELEASE(g_pMesh); SAFE_RELEASE(g_pd3dDevice); SAFE_RELEASE(g_pTextAdaperName) SAFE_RELEASE(g_pTextHelper) SAFE_RELEASE(g_pTextInfor) SAFE_RELEASE(g_pTextFPS) SAFE_RELEASE(g_pd3dDevice)}

在程序中,按下数字键1,2可以进行alpha值大小的调节。

这次的人物模型来自真三国无双六,其实真的很精美的,但是为了演示Alpha混合的效果,就不得不出现一些毁三观的图。

以下五张,分别是五张不同的alhpa值时的截图,大家可以重点看一下变化:












最后还是看一张贴上材质和纹理效果后的正常的图吧,还原我们的三观:





文章最后,依旧是放出本篇文章配套源代码的下载:


本节笔记配套源代码请点击这里下载:


【浅墨DirectX提高班】配套源代码之二十一下载 (新浪微盘)


【浅墨DirectX提高班】配套源代码之二十一下载 (CSDN下载频道)

 
 


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


改变,改变。自己为什么要去改变?

因为不满现状,因为有一颗雄心,有一个和现在环境不能吻合的梦想!


 

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

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



 

------------------------------------------------------------------------------------------------------------------------------

浅墨历时一年为游戏编程爱好者锻造的著作:《逐梦旅程:Windows游戏编程之从零开始》

如果你喜欢浅墨写的【Visual C++】游戏开发系列博客文章,那么你一定会爱上这本书。

这是浅墨专门为热爱游戏编程的朋友们写的入门级游戏编程宝典。



------------------------------------------------------------------------------------------------------------------------------


 

           

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值