【Visual C++】游戏开发笔记四十五 浅墨DirectX教程十三 深度测试和Z缓存专场

   


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

文章链接: http://blog.csdn.net/zhmxy555/article/details/8607864

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



在游戏三维场景中,想创造出唯美而具画面感和真实感的画面,常常需要绘制大量的物体。而这些物体之间通常都存在着遮挡的关系,离观察点较远的物体,会因为近处物体的遮挡而不可见,或者只有部分可见。在Direct3D中,微软为我们提供了,深度缓存(或称Z缓存),配套着深度测试技术,来专门实现这种效果。下图是本篇文章配套程序的一个截图,可以发现,利用了深度测试,才能让战士与这个矩形墙面水乳交融,表达出这个战士似乎是从墙面中穿越而出的真实视觉效果。


 

 

所以,想要绘制出真实的游戏三维场景,深度测试便是必须之物。

本篇文章里,我们就来探究一下深度测试的方方面面。





一、形象化理解深度测试



首先奉上浅墨一直对深度测试的形象化理解。

把深度测试看做在一口井的井口处向井中观望。把所有物体都赋予一个深度值,放到井中来显示。深度越深的物体,离井口就越远。深度越浅的物体,离井口就越近。井表面的深度值为0。离井口近而深度浅的物体,可能会把离井口远的物体遮挡住。最终显示在屏幕上的开启深度测试后的画面,就如在井口处向井中观望里面物体显示出的遮挡与层次的效果一样。

(每次想到这里,浅墨就会联想起美剧《行尸走肉》第二季里面的农场里的那口井,可惜井中掉下去一个行尸,把井水污染了。。。。。)

当然,离井口的深度就是每个物体在世界坐标系中的矩阵的Z坐标值了。

 

 

然后,我们看一下深度测试的具体概念。



二、深度测试相关概念讲解



而想要理解深度测试,首先需要理解深度缓冲区。

深度缓冲区,也常常称为Z缓存,是Direct3D中用来存储绘制到屏幕上的每个像素点的深度信息的一块内存缓冲区,是一个只含有特定像素深度信息而不含图像数据的表面,深度缓存为最终绘制的图像中的每一个像素都保留了一个深度值,如果我们绘制屏幕的分辨率为800×600像素的话,那么深度缓存的大小也为800×600。

当Direct3D讲一个场景渲染来目标表面上时,它使用深度缓冲区来决定光栅化后各个多边形的像素前后的遮挡关系,最终决定哪个颜色值会被绘制出来。

所以可以这样理解,Direct3D通过比较当前绘制的像素点的深度和对应深度缓冲区的点的深度值来决定是否绘制当前像素。如果深度测试的结果为TRUE,就会绘制当前像素,并用当前像素点的深度值来更新一下深度缓冲区,反之,则不予绘制。而在通常情况下,深度缓冲区对应于屏幕大小的一块二维区域。

比如,我们在三维场景中同时绘制了一把长剑和一个盾牌两个立体模型。如果长剑的深度值为30,盾牌的深度值为60(  如取景变换中摄像机的位置矩阵在Z轴上为负值D3DXVECTOR3 vEye(0.0f, 1.0, -100.0f),长剑模型的世界矩阵为(0.0f,0.0f,30.0f),盾牌的世界矩阵为(0.0f,0.0f,60.0f)),那么就是表示长剑的盾牌的前面。Direct3D会首先绘制长剑和盾牌中的其一,而当绘制剩下的那个3D模型的时候,Direct3D会将当前3D模型位于同一位置的像素与已经绘制的像素(如果两者在该位置都有像素的话)进行测试,若当前像素比原来的像素更近(即深度值更小),那么将用当前像素来更新掉原来的像素(前提是),否则不予更新。

 

当Direct3D将一个场景渲染到目标表面的时候,它使用深度缓冲区决定光栅化后每个多边形的像素前后的遮挡关系,最终决定哪个颜色值被绘制出来,对于一个启用了深度缓冲区的场景进行光栅化操作时,表面上的每个值都要进行深度测试。

 

 

 

 

三、深度测试使用四步曲



在Direct3D中,使用深度测试有一个四步曲,但是这仅仅是一个按部就班的四步曲,后三步的顺序可以随意,只要在渲染前就可以了。而且由于Direct3D默认开启深度测试,甚至后面三步根本就不需要,就可以在程序中畅通无阻地使用深度测试。

但是,我们本着学知识的态度,依然是要介绍并掌握一下,这样在实现某些特殊渲染效果的时候才有“招”可用。就像现在有了游戏引擎,想要快速做一款游戏的话,完全可以去直接用现成的游戏引擎。但是那毕竟是别人的东西,是别人写出来的东西。如果本着学知识的态度的话,我们应该了解引擎的底层实现,知其然并知其所以然。这样,日积月累,才会显得功力比那些做表层学问的人们来得深厚,才能成为真正的技术大牛,实现起游戏画面优化来游刃有余,甚至有能力写出属于自己的游戏引擎。

好像一下扯远了,囧,还是拉回来吧。深度测试使用四步曲如下:


1.创建深度缓冲区

2.开启深度测试

3.设置深度测试函数

4.更新深度缓冲区

 

下面我们分别来详细讲解。

 

1.创建深度缓冲区


关于深度缓冲区的创建,因为是在Direct3D初始化时顺手创建的,所以我们在之前讲解Direct3D初始化时,在《【Visual C++】游戏开发笔记三十三 浅墨DirectX提高班之二 化腐朽为神奇:DirectX初始化四步曲》一文"Direct3D初始化四步曲之三:填内容"中就有提到。

回忆之前的Direct3D初始化四步曲知识,四步曲之三,其实从头到尾其实就是在填充一个D3DPRESENT_PARAMETERS结构体,下面我们先贴出这个结构体的原型:

 

typedef struct D3DPRESENT_PARAMETERS {
 UINT               BackBufferWidth;
 UINT               BackBufferHeight;
 D3DFORMAT           BackBufferFormat;
 UINT               BackBufferCount;
 D3DMULTISAMPLE_TYPE MultiSampleType;
 DWORD              MultiSampleQuality;
 D3DSWAPEFFECT       SwapEffect;
 HWND                hDeviceWindow;
 BOOL                Windowed;
 BOOL                EnableAutoDepthStencil;
 D3DFORMAT          AutoDepthStencilFormat;
 DWORD               Flags;
 UINT               FullScreen_RefreshRateInHz;
 UINT               PresentationInterval;
} D3DPRESENT_PARAMETERS,*LPD3DPRESENT_PARAMETERS;


其中的第十和第十一个参数是我们今天深度测试相关的。

       

◆第十个参数,BOOL 类型的EnableAutoDepthStencil,表示Direct3D是否为应用程序自动管理深度缓存,这个成员为TRUE的话,表示需要自动管理深度缓存,这时候就需要对下一个成员AutoDepthStencilFormat进行相关像素格式的设置。

◆第十一个参数,D3DFORMAT类型的AutoDepthStencilFormat,上面刚介绍过,如果我们把EnableAutoDepthStencil成员设为TRUE的话,在这里就需要指定AutoDepthStencilFormat的深度缓冲的像素格式。具体格式可以在结构体D3DFORMAT中进行选取。我们列举一些可以选取的值:

D3DFMT_D16 深度缓存用16位存储每个像素的深度值

D3DFMT_D24X8 深度缓存用24位存储每个像素的深度值

D3DFMT_D32深度缓存用32位存储每个像素的深度值

 

想要在之后绘制时使用深度测试的话,在这里就要把第十个参数EnableAutoDepthStencil设为true,表示让Direct3D创建并自行管理一个深度缓冲区。而第十一个参数AutoDepthStencilFormat用于设置一下深度缓存的格式,16位,24位,32位等等格式任选。


2.开启深度测试


这一步的主角依旧是我们的老朋友SetRenderState,我们在之前已经用过它和介绍过他无数遍了。因为这个函数的一个参数可以取各式各样的渲染状态和类型,所以注定了它是一个多面手。下面将要介绍的深度使用四步曲的后三步,全都拜倒在了SetRenderState的“石榴裙”之下。

先看一下我们正在讲解的第二步,开启深度测试。自然就是给SetRenderState两个参数取不同的值就可以了。将第一个参数设为D3DRS_ZENABLE,表示第二个参数将对深度测试的开启或者关闭进行设置,第二个参数设为TRUE或者FALSE,表示开启或者关闭深度测试。

我们这步的目的当然就是要开启深度测试,也就是这样写: 

g_pd3dDevice->SetRenderState(D3DRS_ZENABLE,true);  //开启深度测试


当然,要关闭深度测试的话,把第二个参数取false就可以了: 

g_pd3dDevice->SetRenderState(D3DRS_ZENABLE,false); //关闭深度测试


3.设置深度测试函数


还是那个神通广大的SetRenderState函数。对应于这一步的任务,我们把它的第一个参数设为D3DRS_ZFUNC,第二个参数设为想要进行使用的深度测试函数,在D3DCMPFUNC枚举类型中取值,这个D3DCMPFUNC枚举类型我们可以在MSDN中查到定义如下:

 

typedef enum D3DCMPFUNC {
 D3DCMP_NEVER          = 1,
 D3DCMP_LESS           = 2,
 D3DCMP_EQUAL          = 3,
 D3DCMP_LESSEQUAL      = 4,
 D3DCMP_GREATER        = 5,
 D3DCMP_NOTEQUAL       = 6,
 D3DCMP_GREATEREQUAL   = 7,
 D3DCMP_ALWAYS         = 8,
 D3DCMP_FORCE_DWORD    = 0x7fffffff
} D3DCMPFUNC, *LPD3DCMPFUNC;


 

下面我们通过一个表格,对这些枚举类型中的成员进行讲解说明:

枚举类型值

精析

D3DCMP_NEVER

深度测试函数总是返回FALSE

D3DCMP_LESS

测试点深度值小于深度缓冲区中相应值时,返回TRUE,为默认值

D3DCMP_QUAL

测试点深度值等于深度缓冲区中相应值时,返回TRUE

D3DCMP_LESSEQUAL

测试点深度值大于等于深度缓冲区中相应值时,返回TRUE

D3DCMP_GREATER

测试点深度值大于深度缓冲区中相应值时,返回TRUE

D3DCMP_NOTEQUAL

测试点深度值不等于深度缓冲区中相应值时,返回TRUE

D3DCMP_GREATEREQUAL

测试点深度值大于等于深度缓冲区中相应值时,返回TRUE

D3DCMP_ALWAYS

深度测试函数总是返回TRUE

D3DCMP_FORCE_DWORD

这个枚举值一般不用,保证将D3DCMPFUNC枚举类型编译为32位

 

一般我们都将深度测试函数设为D3DCMP_LESS,表示当测试点深度值小于深度缓冲区中相应值时,通过测试并绘制相关像素,这样没有被遮挡的物体才显示,被遮挡住的物体是不显示的。

这步里面也就是一句SetRenderState代码:

 

g_pd3dDevice->SetRenderState(D3DRS_ZFUNC,D3DCMP_LESS);


 

4.更新深度缓冲区


配合第三步设置的深度测试函数,还需要设置深度测试成功时对深度缓冲区如何操作,是保持原来的深度值“按兵不动”呢,还是用当前像素的深度值来更新对应的数值“与时俱进”。

还是那个神通广大的SetRenderState函数。对应于这一步的任务,我们把它的第一个参数设为D3DRS_ZWRITEENABLE,表示在第二个参数里面将对深度缓冲区更改与否做出选择。第二个参数设为TURE的话,表示深度测试成功之后,用当前像素的深度值更新深度缓冲区对应的数值。第二个参数设为TURE是设置更新深度缓冲区时最常用的设置,同时也是默认值。反之,第二个参数设为FALSE,则表示尽管深度测试成功,还是不更新深度缓冲区对应的数值。

代码书写方面,就是这样写:

g_pd3dDevice->SetRenderState(D3DRS_ZWRITEENABLE,true);   //深度测试成功后,更新深度缓存


四、文章配套程序源代码赏析

 

 

本篇文章的配套源代码依旧是包含四个文件,主要用于公共辅助宏定义的D3DUtil.h,用于封装了DirectInput输入控制API的DirectInputClass.h和DirectInputClass.cpp最后才是核心代码main.cpp。

其实D3DUtil.h,DirectInputClass.h以及DirectInputClass.cpp在上篇文章的配套demo的基础上并没有做任何修改,我们只是修改了main.cpp中的代码而已。鉴于这个三个文件在之前的基础上无任何修改且在前面的文章中已经贴出过多次,这次就不再费篇幅贴出了,我们只贴出核心代码main.cpp即可:


//*****************************************************************************************
//
//【Visual C++】游戏开发笔记系列配套源码四十五 浅墨DirectX教程十三 深度测试与Z缓存专场
//		 VS2010版
// 2013年 2月24日  Create by 浅墨 
//图标及图片素材: 《仙剑奇侠传五前传》 龙溟
//
//***************************************************************************************** 


//*****************************************************************************************
// Desc: 宏定义部分   
//*****************************************************************************************
#define SCREEN_WIDTH	800						//为窗口宽度定义的宏,以方便在此处修改窗口宽度
#define SCREEN_HEIGHT	600							//为窗口高度定义的宏,以方便在此处修改窗口高度
#define WINDOW_TITLE	_T("【Visual C++】游戏开发笔记系列配套源码四十五 浅墨DirectX教程十三 深度测试与Z缓存专场") //为窗口标题定义的宏



//*****************************************************************************************
// Desc: 头文件定义部分  
//*****************************************************************************************                                                                                       
#include <d3d9.h>
#include <d3dx9.h>
#include <tchar.h>
#include <time.h> 
#include "DirectInputClass.h"



//*****************************************************************************************
// Desc: 库文件定义部分  
//***************************************************************************************** 
#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") 




//*****************************************************************************************
// Desc: 全局变量声明部分  
//*****************************************************************************************
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;    // 材质的数目

LPD3DXMESH          g_pMeshWall     = NULL; // 墙面网格对象
D3DMATERIAL9        g_MaterialsWall;  // 材质


//*****************************************************************************************
// Desc: 全局函数声明部分 
//***************************************************************************************** 
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();


//*****************************************************************************************
// Name: WinMain( )
// Desc: 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"A Watchful Guardian.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;  
}



//*****************************************************************************************
// Name: WndProc()
// Desc: 对窗口消息进行处理
//*****************************************************************************************
LRESULT CALLBACK WndProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam )   //窗口过程函数WndProc
{
	switch( 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消息
		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;					//正常退出
}


//*****************************************************************************************
// Name: Direct3D_Init( )
// Desc: 初始化Direct3D
// Point:【Direct3D初始化四步曲】
//		1.初始化四步曲之一,创建Direct3D接口对象
//		2.初始化四步曲之二,获取硬件设备信息
//		3.初始化四步曲之三,填充结构体
//		4.初始化四步曲之四,创建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            = 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;
}


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"SwordMan.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_pTextures[i]  = NULL;
		D3DXCreateTextureFromFileA(g_pd3dDevice, pMtrls[i].pTextureFilename, &g_pTextures[i]);
	}

	SAFE_RELEASE(pAdjBuffer)
	SAFE_RELEASE(pMtrlBuffer)


	//用D3DXCreateBox来创建一个极薄的屏障
	D3DXCreateBox(g_pd3dDevice, 30.0f, 30.0f, 0.5f, &g_pMeshWall, NULL);
	g_MaterialsWall.Ambient  = D3DXCOLOR(0.8f, 0.2f, 0.1f, 1.0f);
	g_MaterialsWall.Diffuse  = D3DXCOLOR(1.0f, 1.0f, 1.0f, 1.0f);
	g_MaterialsWall.Specular = D3DXCOLOR(0.1f, 0.1f, 0.1f, 1.0f);



	// 设置光照
	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);     //深度测试成功后,更新深度缓存


	return S_OK;
}


//*****************************************************************************************
// Name:Matrix_Set()
// Desc: 设置世界矩阵
// Point:【Direct3D四大变换】
//		1.【四大变换之一】:世界变换矩阵的设置
//		2.【四大变换之二】:取景变换矩阵的设置
//		3.【四大变换之三】:投影变换矩阵的设置
//		4.【四大变换之四】:视口变换的设置
//*****************************************************************************************
void Matrix_Set()
{

	//--------------------------------------------------------------------------------------
	//【四大变换之二】:取景变换矩阵的设置
	//--------------------------------------------------------------------------------------
	D3DXMATRIX matView; //定义一个矩阵
	D3DXVECTOR3 vEye(0.0f, 0.0f, -150.0f);  //摄像机的位置
	D3DXVECTOR3 vAt(0.0f, 0.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); //视口的设置

}


void				Direct3D_Update( HWND hwnd)
{
	//使用DirectInput类读取数据
	g_pDInput->GetInput();

	// 开启或者关闭深度测试
	if (g_pDInput->IsKeyDown(DIK_1))  //按下1键,开启深度测试
		g_pd3dDevice->SetRenderState(D3DRS_ZENABLE, true);
	if (g_pDInput->IsKeyDown(DIK_2))  //按下2键,关闭深度测试
		g_pd3dDevice->SetRenderState(D3DRS_ZENABLE, false);


	// 按住鼠标左键并拖动,为平移操作
	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.005f;
	if (g_pDInput->IsKeyDown(DIK_D)) fPosX += 0.005f;
	if (g_pDInput->IsKeyDown(DIK_W)) fPosY += 0.005f;
	if (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.005f;
	if (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();
}



//*****************************************************************************************
// Name: Direct3D_Render()
// Desc: 进行图形的渲染操作
// Point:【Direct3D渲染五步曲】
//		1.渲染五步曲之一,清屏操作
//		2.渲染五步曲之二,开始绘制
//		3.渲染五步曲之三,正式绘制
//		4.渲染五步曲之四,结束绘制
//		5.渲染五步曲之五,翻转显示
//*****************************************************************************************

void Direct3D_Render(HWND hwnd)
{

	//--------------------------------------------------------------------------------------
	// 【Direct3D渲染五步曲之一】:清屏操作
	//--------------------------------------------------------------------------------------
	g_pd3dDevice->Clear(0, NULL, D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER, D3DCOLOR_XRGB(60, 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);  //绘制此部分
	}

	// 绘制墙面
	D3DXMATRIX matWorld;   //定义一个矩阵为准备为墙面的世界矩阵	
	D3DXMatrixTranslation(&matWorld, 0.0f,0.0f,-50.0f);//给墙面的世界矩阵初始化,Z坐标设为-50.0f
	g_pd3dDevice->SetTransform(D3DTS_WORLD, &matWorld);//设置墙面的世界矩阵
	g_pd3dDevice->SetMaterial(&g_MaterialsWall);//设置材质
	g_pMeshWall->DrawSubset(0); //绘制墙面


			//在窗口右上角处,显示每秒帧数
			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:开启或者关闭深度测试", -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);  // 翻转与显示
	 
}




//*****************************************************************************************
// Name:Get_FPS()函数
// Desc: 用于计算帧速率
//*****************************************************************************************
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;
}



//*****************************************************************************************
// Name: Direct3D_CleanUp()
// Desc: 对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_pMeshWall);
	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在开启深度测试和关闭深度测试之间切换。

我们在这个程序中绘制了两个物体,一个当然就是那个帅气的战士,另外就是一面巍然不动的褐色的矩形墙,本来还可以把这面墙弄成真正的贴过图后的砖块墙的,但是那样未免会增加一些代码量,让大家理解的难度上升,于是就用了这个D3DXCreateBox函数来图个方便,快捷绘制一面单色的墙。

帅气的战士的世界矩阵会随着我们鼠标和键盘的输入而有所改变,其世界矩阵初始值为(0.0f,-50.0f, 0.0f)。

而单色的墙世界矩阵始终被我们设为了(0.0f,0.0f,-50.0f).

对于深度测试,一般我们只用看世界矩阵的Z坐标。程序中我们摄像机的位置被我设为了(0.0f, 0.0f, -150.0f)。我们只用看摄像机的Z坐标为-150.所以我们视角的方向为垂直屏幕向里,深度越深,Z轴坐标在不断增大。

战士的初始世界矩阵Z坐标为 0.0f,单色墙的世界矩阵Z坐标为-50.0f。所以开启深度测试后,战士在我们不动键盘鼠标让其移动的情况下,会被单色墙遮挡住。我们滑动鼠标上的滚轮,让战士的Z坐标小于-50.0f之后,会发现战士完全位于单色墙之前了,不再被其遮挡。

而如果我们按下键盘上的2键,关闭深度测试的话,会发现单色墙一直拦在战士的身前,战士本来披于身后被面部遮挡住的一头马尾金发辫,竟然可以从正面看到。。非常地不科学。。。


下面先贴出一些程序截图:





开启深度测试后,这个战士看起来还是非常帅气的,但是接下来我们来看看一些开启/关闭深度测试的对比图。

开启了深度测试的状态:


关闭深度测试的状态:


看到辫子的异样了吗,还有一直横亘在战士身前的讨厌的单色矩形。

再看一组吧。

开启了深度测试的状态:


关闭了深度测试的状态:



大家看到这奇葩的头发了吗?这货这么帅气,一世英名就毁在没开深度测试上面了。。。。。



大家可要注意了,我们在写3D游戏程序时难免会载入很多3D模型,如果不开深度测试的话,且不说不同3D模型之间不会有正确的遮挡显示效果,就连3D模型本身都会缺失真实的深度感,看起来怪模怪样。

另外,不开深度测试,后绘制的物体永远显示在先绘制物体的前面,这是需要注意一个常识。这正好解释了为什么后绘制的单色墙为什么在不开深度测试的情况下永远横艮于战士之前。

也就是说,如果不开深度测试,如果两个物体在某个像素点有交集,那么后绘制的物体会一直挡住先绘制的物体。就像我们在一张白纸上画画,先画的人物就在最底层,后画的人物会覆盖住先画的人物的笔迹一样。

最后提一点,Direct3D渲染五步曲的第一步里面用到的那个Clear方法里面也有和深度测试相关的内容,大家可以留心一下。


 好了。这篇文章也就告一段落了。下篇文章如果不出意外,应该会开始讲解可编程渲染流水线了。Direct3D还真是博大精深,一时半会儿还真是讲不完。

另外。从本篇文章的配套程序开始,都会配上循环播放的背景音乐,通过PlaySoundAPI函数来实现,在Main函数中加一句这个即可:

PlaySound(L"A WatchfulGuardian.wav", NULL, SND_FILENAME | SND_ASYNC|SND_LOOP);   //循环播放背景音乐

本篇的背景音乐是浅墨最喜欢的电影神作《蝙蝠侠前传2:黑暗骑士》原声带中的一曲恢弘的《AWatchful Guardian》,最高音质的wav格式本来需要100多Mb,浅墨为了缩减示例代码的大小,就把音质换为8bit每秒,采样频率也降低为了8000Hz,这样曲子就缩减到了6Mb,但是音质也大大打了折扣,囧。贴一张电影海报吧:




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

 

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


【浅墨DirectX提高班】配套源代码之十三下载 


 

 



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

浅墨在这里,希望喜欢游戏开发系列文章的朋友们能留下你们的评论,每次浅墨登陆博客看到大家的留言的时候都会非常开心,感觉自己正在传递一种信仰,一种精神。 


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


假如一个人寿命为60岁,那么他总共有21900天。一生时间的用途分别为:睡20年(7300天);吃饭6年(2190天);穿衣和梳洗5年(1825天);生病3年(1095天);打电话1年(365天);照镜子70天;抹鼻涕10天。最后只剩下3205天,即8年又285天,做你真正想做的事情。珍惜大好时光吧~





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

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

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

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

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

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



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




  • 33
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 33
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值