继续学习DX9。为什么不学DX11呢,因为我想先从过去的一个经典版本学习领悟其中的原理,再来了解新的版本。一贯目标:加深对游戏开发的底层知识的理解。
今天总结下着色模式、索引缓存。
首先,要记录的是,D3D编程的一大部分工作就是设置合适的渲染状态。一种渲染状态会一直起作用,直到你下一次改变渲染状态为止。D3D通过调用函数:SetRenderState()来设置渲染状态。
一、着色模式
D3D中物体表面是由许多多边形构成的,当渲染一个多边形时,不同的着色模式,会有不同的视觉效果。着色模式决定了多边形上每个点的颜色和光照的强度。
D3D中有两种着色模式:平面着色(FLAT)和高洛德法着色模式(GOURAUD)
1.平面着色
在该模式下,D3D绘制流水线使用多边形的第一个顶点的材质颜色作为整个多边形的颜色来渲染该多边形。如果多边形不共面,那么渲染后多边形之间将具有可视的清晰边缘。
2.高洛德着色
在该模式下,它使用顶点法线和光照参数为每个顶点计算颜色。然后,穿越多边形的表面进行插值,插值以线性方式完成。
两种模式的比较:大部分情况下,高洛德着色模式更好。因为它采用插值的方式,使多边形的边缘看起更加平滑,看起更加真实。D3D默认选择的着色模式是高洛德模式。
二、索引缓存
试想一个由很多三角形组成的一个物体,比如一个高模,上万的面数,显然有很多顶点是重复在一起的,如果不进行优化,那么渲染的时候,要浪费多少的内存!所以有了索引缓存,避免过多的重复顶点的存在,提高渲染的效率。
使用索引缓存的方法:先创建一个顶点缓存,将不重复的顶点数据写入顶点缓存;接下来创建一个顶点索引缓存,存放每个三角形的定点索引信息;最后通过定点索引和顶点数据共同完成三角形的绘制。
代码:
//-----------------------------------------------------------------------------
// File: CreateDevice.cpp
//
// Desc: This is the first tutorial for using Direct3D. In this tutorial, all
// we are doing is creating a Direct3D device and using it to clear the
// window.
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//-----------------------------------------------------------------------------
#include <d3d9.h>
#pragma warning( disable : 4996 ) // disable deprecated warning
#include <strsafe.h>
#pragma warning( default : 4996 )
#include <d3dx9core.h> //新增
//-----------------------------------------------------------------------------
// Global variables
//-----------------------------------------------------------------------------
LPDIRECT3D9 g_pD3D = NULL; //Direct3d对象
LPDIRECT3DDEVICE9 g_pd3dDevice = NULL; // Direct3D渲染设备对象
LPD3DXFONT g_pFont = 0; //字体对象
WCHAR* strText = L" 鼠标左键控制着色模式\n\
右键控制填充模式\
";
//"\n是换行用的,然后再加一个"\"是说明该字符串内容没有结束,下一行还是该字符串的内容"
LPDIRECT3DINDEXBUFFER9 g_pIB =NULL; //索引缓存对象
bool g_bFlat = false;//着色模式
int g_iFillmode = 3; //填充模式:1点2线3面
RECT clientRect;
LPDIRECT3DVERTEXBUFFER9 g_pVB = NULL; //顶点缓存对象
//int g_iType = 1; //图元类型,每个图元对应一个整形数值,存放于该变量中,初始化绘制的是整形“1”对应的图元
//定义灵活顶点结构
struct CUSTOMVERTEX
{
FLOAT x,y,z,rhw; //定义了坐标信息
DWORD color;
};
#define D3DFVF_CUSTOMVERTEX (D3DFVF_XYZRHW | D3DFVF_DIFFUSE)
HRESULT InitVBandIB()
{
//1、设置顶点数据
CUSTOMVERTEX g_Vertices[9];
g_Vertices[0].x = 150;
g_Vertices[0].y = 125;
g_Vertices[0].z = 0.5f;
g_Vertices[0].rhw = 1.0f;
g_Vertices[0].color = 0xff00ff00;
for(int i = 0;i<8;i++)
{
g_Vertices[i+1].x = (float)(80 * sin(i*3.14159/4.0))+150;
g_Vertices[i+1].y = -(float)(80 * cos(i*3.14159/4.0))+125;
g_Vertices[i+1].z = 0.5f;
g_Vertices[i+1].rhw = 1.0f;
if(i%2==0)
g_Vertices[i+1].color = 0xffff0000;
else g_Vertices[i+1].color = 0xff0000ff;
}
//2、设置顶点索引数组
WORD g_Indices[] = {0,1,2,0,2,3,0,3,4,0,4,5,
0,5,6,0,6,7,0,7,8,0,8,1};
//创建顶点缓存
if(FAILED(g_pd3dDevice->CreateVertexBuffer(9*sizeof(CUSTOMVERTEX),0,D3DFVF_CUSTOMVERTEX,D3DPOOL_DEFAULT,&g_pVB,NULL)))
{
return E_FAIL;
}
//填充顶点缓存
VOID* pVertices;
if(FAILED(g_pVB->Lock(0,sizeof(g_Vertices),(void**)&pVertices,0)))
return E_FAIL;
memcpy(pVertices,g_Vertices,sizeof(g_Vertices));
g_pVB->Unlock();
//创建索引缓存
if(FAILED(g_pd3dDevice->CreateIndexBuffer(24*sizeof(WORD),0,D3DFMT_INDEX16,D3DPOOL_DEFAULT,&g_pIB,NULL)))
{
return E_FAIL;
}
//填充索引缓存
VOID* pIndices;
if(FAILED(g_pIB->Lock(0,sizeof(g_Indices),(void**)&pIndices,0) ))
{
return E_FAIL;
}
memcpy(pIndices,g_Indices,sizeof(g_Indices));
g_pIB->Unlock();
return S_OK;
}
/*
说明,一个Direct3D对象可以创建多个Direct3d设备对象,但是D3D对象创建的所有渲染设备对象共享相同的物理资源,
所以会导致严重的性能下降
*/
//-----------------------------------------------------------------------------
// Name: InitD3D()
// Desc: Initializes Direct3D
//-----------------------------------------------------------------------------
HRESULT InitD3D( HWND hWnd )
{
// 创建D3D对象,该对象用于创建D3D设备对象
if( NULL == ( g_pD3D = Direct3DCreate9( D3D_SDK_VERSION ) ) )
return E_FAIL;
//设置D3DPRESENT_PARAMETERS结构,准备创建D3D设备对象
/*说明 在DX对窗口进行渲染之前,必须先根据渲染需要对窗口进行设置,比如是使用硬件渲染还是软件渲染,渲染是使用单缓冲还是双缓冲
是否需要进行深度缓存,是否需要模板、Alpha缓存等。当你为窗口设置了这些参数之后,就不能改变了,除非销毁再根据需要创建另外的窗口。
窗口的3d特征是一次性设置的,通常就在创建窗口之后,这些设置的集合的名称就是D3DPRESENT_PARAMETERS
*/
D3DPRESENT_PARAMETERS d3dpp;
ZeroMemory( &d3dpp, sizeof( d3dpp ) );
d3dpp.Windowed = TRUE; //全屏或窗口
d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;//指定表面在交换链中是如何被交换的。D3DSWAPEFFECT_DISCARD:后备缓存的内容被复制到屏幕上后,后备缓存的内容失效,可以丢弃
d3dpp.BackBufferFormat = D3DFMT_UNKNOWN;//后备缓冲的格式,若不熟悉,可设置为D3DFMT_UNKNOWN,这时它将使用桌面的格式
/*
当准备工作结束后,就可以创建DX设备对象罗,因为DX对象是整个DX程序的最顶层对象,其他的对象都将通过它的接口函数调用来创建
参数1:指定对象要表示的物理显示设备,即使用哪个显卡来创建设备对象
参数2:设备类型,这里选的是HAL设备。最主要的设备就是硬件抽象层(HAL)设备,它支持硬件光栅化加速和软硬件顶点处理。D3D通过HAL访问硬件。应用程序
如果在支持HAL的计算机上运行,那么通过使用HAL设备将获得最佳性能。另一个是参考设备,它支持所有D3D特性,但是用软件实现,所以对于硬件来说,较慢。
参数3:指定DX程序从前台变换到后台时的提示窗口。
参数4:指定D3D进行3D运算的工作方式,此处参数含义:DX软件进行顶点运算
参数5:制定一个已经初始化的D3DPRESENT_PARAMETERS实例
参数6:返回创建的设备,创建好的设备对象指针存储在该参数中
*/
if( FAILED( g_pD3D->CreateDevice( D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWnd,
D3DCREATE_SOFTWARE_VERTEXPROCESSING,
&d3dpp, &g_pd3dDevice ) ) )
{
return E_FAIL;
}
//-------------字体部分-----------------
//创建字体对象
// Device state would normally be set here
if( FAILED(D3DXCreateFont(g_pd3dDevice,0,0,0,0,0,0,0,0,0,L"Arial",&g_pFont)))
return E_FAIL;
//获取窗口客户区
GetClientRect(hWnd,&clientRect);
//--------------顶点部分-----------------
//设置剔除模式为不剔除任何面
g_pd3dDevice->SetRenderState(D3DRS_CULLMODE,D3DCULL_NONE);
//设置图元填充模式为线框模式
g_pd3dDevice->SetRenderState(D3DRS_FILLMODE,D3DFILL_WIREFRAME);
//
return S_OK;
}
//-----------------------------------------------------------------------------
// Name: Cleanup()
// Desc: Releases all previously initialized objects
// 清空之前引用的资源
//-----------------------------------------------------------------------------
/*
D3D对象和D3D设备对象都是系统资源,若只申请不释放,必定造成资源泄露,在整个过程,申请的对象一定要手动释放。
因为D3D设备对象是D3D对象创建的,所以先释放D3D设备对象。最先创建的最后释放,创建、释放的过程和栈结构一样
*/
VOID Cleanup()
{
if( g_pd3dDevice != NULL )
g_pd3dDevice->Release();
if( g_pD3D != NULL )
g_pD3D->Release();
//释放字体
if(g_pFont != NULL)
{
g_pFont->Release();
}
//释放顶点缓存对象
if(g_pVB != NULL)
{
g_pVB->Release();
}
//释放顶点缓存对象
if(g_pVB != NULL)
g_pVB->Release();
}
//-----------------------------------------------------------------------------
// Name: Render()
// Desc: Draws the scene
//-----------------------------------------------------------------------------
VOID Render()
{
//设置着色模式
if(g_bFlat)
g_pd3dDevice->SetRenderState(D3DRS_SHADEMODE,D3DSHADE_FLAT); //平面着色
else
g_pd3dDevice->SetRenderState(D3DRS_SHADEMODE,D3DSHADE_GOURAUD); //高洛德着色
//设置填充模式
if(g_iFillmode == 1) //点
g_pd3dDevice->SetRenderState(D3DRS_FILLMODE,D3DFILL_POINT);
else if(g_iFillmode == 2) //线填充
g_pd3dDevice->SetRenderState(D3DRS_FILLMODE,D3DFILL_WIREFRAME);
else if(g_iFillmode == 3) //面填充
g_pd3dDevice->SetRenderState(D3DRS_FILLMODE,D3DFILL_SOLID);
//清空后台缓存
g_pd3dDevice->Clear(0,NULL,D3DCLEAR_TARGET,D3DCOLOR_XRGB(0,0,0),1.0F,0);
//开始在后台缓存绘制图形
if(SUCCEEDED(g_pd3dDevice->BeginScene()))
{
g_pFont->DrawText(NULL,strText,(int)wcslen(strText),&clientRect,DT_NOCLIP | DT_LEFT | DT_TOP,0xffffffff);
//在后台缓存绘制图形
g_pd3dDevice->SetStreamSource(0,g_pVB,0,sizeof(CUSTOMVERTEX));
g_pd3dDevice->SetFVF(D3DFVF_CUSTOMVERTEX);
g_pd3dDevice->SetIndices(g_pIB);//设置索引缓存(索引缓存绘制需要设置当前的索引缓存,这容易被忽视)
g_pd3dDevice->DrawIndexedPrimitive(D3DPT_TRIANGLELIST,0,0,9,0,8);
}
// End the scene
g_pd3dDevice->EndScene();
// 将在后台缓存绘制的图形提交到前台缓存显示
/*
参数1:是你想要显示的后备缓存的一个矩形区域,设为NULL表示要将整个后备缓存的内容显示
参数2:表示一个显示区域,设为NULL表示整个客户显示区域
参数3:可以通过它来将内容显示到不同的窗口中。设为NULL则表示显示到当前主窗口。
参数4:是最小更新区域指针,一般设为NULL
*/
g_pd3dDevice->Present( NULL, NULL, NULL, NULL );
}
//-----------------------------------------------------------------------------
// Name: MsgProc()
// Desc: The window's message handler
// 消息处理函数 回调函数
//-----------------------------------------------------------------------------
LRESULT WINAPI MsgProc( HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam )
{
switch( msg )
{
//窗口关闭
case WM_DESTROY:
Cleanup();
PostQuitMessage( 0 );
return 0;
//窗口绘制
case WM_PAINT:
Render();
//ValidateRect( hWnd, NULL );
return 0;
case WM_LBUTTONDOWN: //单击鼠标左键切换着色模式
g_bFlat = !g_bFlat;
Render();
//ValidateRect( hWnd, NULL );
return 0;
case WM_RBUTTONDOWN: //单击鼠标右键切换填充模式
if(g_iFillmode == 1) g_iFillmode = 2;
else if(g_iFillmode == 2) g_iFillmode = 3;
else if(g_iFillmode == 3) g_iFillmode = 1;
Render();
//ValidateRect( hWnd, NULL );//使窗口有效 清除消息队列中的WM_PAINT消息
return 0;
}
return DefWindowProc( hWnd, msg, wParam, lParam );
}
//-----------------------------------------------------------------------------
// Name: wWinMain()
// Desc: 这是整个工程的入口函数,从这里开始理结构,,
/*
注意,InitializeD3D是完成DX整个框架的资源需求,如果该函数不成功,这个程序就不能继续运行,但
如果Windows框架资源没有申请成功,这个程序也不能继续运行。应该先申请Windows框架资源,然后再判断
InitializeD3D是否创建成功,若成功,则进入消息循环
-----------------------------
wWinMain可以处理unicode字符,
而WinMain却会把Unicode字符直接转换成ANSI字符来处理(
如果使用了中文,就可能出错)
第一个参数:应用程序当前句柄
第二个:这一个应用程序实例的前一个句柄(同时运行了一个以上)
第三:不包含应用程序的命令行,可以用CMD向应用程序传入一条字符串指令
第四:制定窗口如何显示的ID
*/
//-----------------------------------------------------------------------------
INT WINAPI wWinMain( HINSTANCE hInst, HINSTANCE, LPWSTR, INT )
{
UNREFERENCED_PARAMETER( hInst );
/*
Win32中的宏UNREFERENCED_PARAMETER,
可以使用它来避免编译器对在函数体中未引用的参数的警告。
虽然从技术上讲是没有必要的,但是努力地构建0个警告的代码是一种良好的编程习惯。
由于这个特殊的宏没有做任何事情,编译器会将对其进行优化。
*/
//定义窗口类
/*
这个窗口类被定义为结构体WNDCLASSEX,它包含Win32窗口的各种属性,
其中包括窗口的图标,菜单,窗口所属的应用实例,光标的外观等等
*/
WNDCLASSEX wc =
{
sizeof( WNDCLASSEX ), CS_CLASSDC, MsgProc, 0L, 0L,
GetModuleHandle( NULL ), NULL, NULL, NULL, NULL,
L"DirectX游戏编程一", NULL
};
//注册窗口类
/*
把我们创建的WNDCLASSEX结构发送给函数RegisterClassEx(),向系统注册窗口类。
函数RegisterClassEx()必须在创建窗口前调用,该函数需要窗口类结构的地址作为
参数才能进行注册。如果该函数执行后,返回值是0就表示注册失败,
读者就要检查这个窗口类各成员的值,看看是否有错误,确保它们都是有效值。
*/
RegisterClassEx( &wc );
/*
下一步是创建实际的窗口
CreateWindow接受Unicode字符。如果使用的是Unicode版本,
我们必须始终要把字符串用引号括起来,然后加一个L做前缀,
以表明我们提供的是Unicode字符。
-----
参数:
lpClassName(可选的)——窗口类的名称(与窗口类结构使用相同的名称)。
lpWindowName (可选的)——窗口的标题栏文字。
dwStyle——窗口风格的标志。
X——窗口的水平位置。
Y——窗口的垂直位置。
nWidth——窗口的宽度。
hHeight——窗口的高度。
hWndParent(可选的)——指向父窗口的句柄(比如这个新的窗口是一个弹出窗口或子窗口)。
hMenu (可选的)——窗口菜单资源句柄。
hInstance (可选)——应用程序实例ID(函数wWinMain的第一个参数)
lpParam (optional)——作为窗口回调函数proc的参数,向窗口传递数据
---
函数CreateWindow(A)的返回值是一个非空的句柄
*/
HWND hWnd = CreateWindow( L"DirectX游戏编程一", L"DirectX游戏编程一:初始化D3D设备",
WS_OVERLAPPEDWINDOW, 100, 100, 300, 300,
NULL, NULL, wc.hInstance, NULL );
// Initialize Direct3D 初始化D3D设备
/*如果创建成功,则进入消息循环*/
if( SUCCEEDED( InitD3D( hWnd ) ) )
{
// 展示窗口
if(SUCCEEDED( InitVBandIB() ))
{
ShowWindow( hWnd, SW_SHOWDEFAULT );
UpdateWindow( hWnd );
// Enter the message loop
/*
MSG是一个Win32结构,用于保存窗口消息,其中一些消息是来自操作系统的,
应用程序将会响应这些消息
*/
MSG msg;
while( GetMessage( &msg, NULL, 0, 0 ) )//提取消息
{
/*
我们可以通过调用TranslateMessage函数和DispatchMessage函数对这一消息作出回应
函数TranslateMessage是将虚拟键消息转换为字符消息,
函数DispatchMessage是派发这个消息到窗口程序的回调函数中去,这个将在下一节中讨论
*/
TranslateMessage( &msg );
DispatchMessage( &msg );
}
}
UnregisterClass( L"DirectX游戏编程一", wc.hInstance );
return 0;
}
}
程序运行截图: