第23章 三维天空的构建
目前描述三维天空的技术主要包括三种类型,直接来介绍使用最广泛的模拟技术,详细的描述可以见作者的博文。
天空盒(Sky Box),即放到场景的是一个立方体。它是目前使用最广泛的三维天空模拟技术,网络上素材丰富,所以这次就用教大家用天空盒来模拟三维天空。天空盒经常是由24个顶点、六个面组成的立方体(或者直接从做好的X模型文件载入天空盒),并经常会随着视点的移动而移动,来刻画极远处玩家无法达到位置的天空
天空盒的设计
1.准备天空盒纹理素材
天空盒的纹理自然就是我们这个天空盒子立方体每个面的纹理了,至少5个面,最多6个面,因为底面处是我们所在的土地,是地形,也就不用渲染为天空了。
这5个面可以分别单独成文件,像这样:
这5张纹理需要满足的条件是:按照规定的几个面拼接起来,能构成一幅360度并包含顶部的无缝衔接的全景图:
2. 天空盒类的设计
给这个类取名为SkyBoxClass。
我们来看下这个类中有哪些内容。
LPDIRECT3DDEVICE9类型的设备接口指针m_pd3dDevice自然不能少。
然后这个类中需要处理24个带纹理坐标的顶点来构成一个立方体盒子,自然少不了FVF灵活顶点格式和一个DIRECT3DVERTEXBUFFER接口的指针。
接着还要有五个纹理对象,分别储存5个面上的纹理图,所以一个LPDIRECT3DTEXTURE9类型的m_pTexture[5]自然也少不了。最后,还需要定义一个float类型的m_Length表示天空盒的边长。结构体和成员变量就是这些了,我们再来看一下需要有哪些成员函数。
首先构造函数析构函数我们写出来,接着再写三个函数就够了,它们分别是初始化天空盒顶点的InitSkyBox函数,加载纹理的LoadSkyTextureFromFile函数,渲染天空盒的RenderSkyBox函数。
SkyBoxClass类的轮廓就是这样了,那么把上面我们的思路实现成代码就是如下,即贴出SkyBoxClass.h中全部代码:
#pragma once
#include "D3DUtil.h" //为天空盒类定义一个FVF灵活顶点格式
struct SKYBOXVERTEX
{
float x,y,z;
float u,v;
};
#define D3DFVF_SKYBOX D3DFVF_XYZ|D3DFVF_TEX1 class SkyBoxClass
{
private:
LPDIRECT3DDEVICE9 m_pd3dDevice; //D3D设备对象
LPDIRECT3DVERTEXBUFFER9 m_pVertexBuffer; //顶点缓存对象
LPDIRECT3DTEXTURE9 m_pTexture[]; //5个纹理接口对象
float m_Length; //天空盒边长 public:
SkyBoxClass( LPDIRECT3DDEVICE9 pDevice ); //构造函数
virtual ~SkyBoxClass(void); //析构函数 public:
BOOL InitSkyBox( float Length ); //初始化天空盒函数
BOOL LoadSkyTextureFromFile(wchar_t *pFrontTextureFile, wchar_t *pBackTextureFile,wchar_t *pLeftTextureFile, wchar_t *pRightTextureFile,wchar_t *pTopTextureFile); //从文件加载天空盒五个方向上的纹理
VOID RenderSkyBox( D3DXMATRIX *pMatWorld, BOOL bRenderFrame ); //渲染天空盒,根据第一个参数设定天空盒世界矩阵,第二个参数选择是否渲染出线框 };
3. 天空盒类的实现
首先是类构造函数,直接对着看类定义中有哪些变量,分别赋初值就行。除了Direct3D设备对象赋值成通过函数形参传进来的设备对象指针pDevice之外,其他的参数根据类型统统取NULL或者0.0f:
// Desc: 构造函数
//-------------------------------------------------------------------------------------------------
SkyBoxClass::SkyBoxClass( LPDIRECT3DDEVICE9 pDevice )
{
//给各个参数赋初值
m_pVertexBuffer=NULL;
m_pd3dDevice=pDevice;
for(int i=; i<; i++)
m_pTexture[i] = NULL;
m_Length = 0.0f;
}
接下来要实现的就是最关键的顶点初始化函数InitSkyBox。首先,通过形参把天空盒的边长传给代表边长的成员函数m_Length。接着就是我们熟悉的顶点缓存使用四步曲的二、三两步——创建顶点缓存、访问顶点缓存了。
与D3D实现的普通立方体贴图不同的一点是,大部分情况下我们视点都包容在天空盒内部,因此,天空盒的顶点顺序应当是正好与我们之前讲的普通立方体的顶点顺序相反。所以,InitSkyBox函数的实现代码就是这样:
BOOL SkyBoxClass::InitSkyBox( float Length )
{
m_Length=Length; //1.创建。创建顶点缓存
m_pd3dDevice->CreateVertexBuffer( * sizeof(SKYBOXVERTEX), ,
D3DFVF_SKYBOX, D3DPOOL_MANAGED, &m_pVertexBuffer, ); //用一个结构体把顶点数据先准备好
SKYBOXVERTEX vertices[] =
{
//前面的四个顶点
{ -m_Length/, 0.0f, m_Length/, 0.0f, 1.0f, },
{ -m_Length/, m_Length/, m_Length/, 0.0f, 0.0f, },
{ m_Length/, 0.0f, m_Length/, 1.0f, 1.0f, },
{ m_Length/, m_Length/, m_Length/, 1.0f, 0.0f, }, //背面的四个顶点
{ m_Length/, 0.0f, -m_Length/, 0.0f, 1.0f, },
{ m_Length/, m_Length/, -m_Length/, 0.0f, 0.0f, },
{ -m_Length/, 0.0f, -m_Length/, 1.0f, 1.0f, },
{ -m_Length/, m_Length/, -m_Length/, 1.0f, 0.0f, }, //左面的四个顶点
{ -m_Length/, 0.0f, -m_Length/, 0.0f, 1.0f, },
{ -m_Length/, m_Length/, -m_Length/, 0.0f, 0.0f, },
{ -m_Length/, 0.0f, m_Length/, 1.0f, 1.0f, },
{ -m_Length/, m_Length/, m_Length/, 1.0f, 0.0f, }, //右面的四个顶点
{ m_Length/, 0.0f, m_Length/, 0.0f, 1.0f, },
{ m_Length/, m_Length/, m_Length/, 0.0f, 0.0f, },
{ m_Length/, 0.0f, -m_Length/, 1.0f, 1.0f, },
{ m_Length/, m_Length/, -m_Length/, 1.0f, 0.0f, }, //上面的四个顶点
{ m_Length/, m_Length/, -m_Length/, 1.0f, 0.0f, },
{ m_Length/, m_Length/, m_Length/, 1.0f, 1.0f, },
{ -m_Length/, m_Length/, -m_Length/, 0.0f, 0.0f, },
{ -m_Length/, m_Length/, m_Length/, 0.0f, 1.0f, }, }; //准备填充顶点数据
void* pVertices;
//2.加锁
m_pVertexBuffer->Lock( , , (void**)&pVertices, );
//3.访问。把结构体中的数据直接拷到顶点缓冲区中
memcpy( pVertices, vertices, sizeof(vertices) );
//4.解锁
m_pVertexBuffer->Unlock(); return TRUE;
}
接下来看看纹理载入函数LoadSkyTextureFromFile的写法;
BOOL SkyBoxClass::LoadSkyTextureFromFile(wchar_t *pFrontTextureFile, wchar_t *pBackTextureFile,wchar_t *pLeftTextureFile, wchar_t *pRightTextureFile,wchar_t *pTopTextureFile)
{
//从文件加载五张纹理
D3DXCreateTextureFromFile( m_pd3dDevice , pFrontTextureFile, &m_pTexture[] ); //前面
D3DXCreateTextureFromFile( m_pd3dDevice , pBackTextureFile, &m_pTexture[] ); //后面
D3DXCreateTextureFromFile( m_pd3dDevice , pLeftTextureFile, &m_pTexture[] ); //左面
D3DXCreateTextureFromFile( m_pd3dDevice , pRightTextureFile, &m_pTexture[] ); //右面
D3DXCreateTextureFromFile( m_pd3dDevice , pTopTextureFile, &m_pTexture[] ); //上面
return TRUE;
}
渲染天空盒RenderSkyBox函数:
RenderSkyBox函数其中用到了纹理阶段混合操作。纹理映射的本质实际上就是从纹理中获取颜色值,然后应用到物体表面上。而以后我们会接触到的多次纹理映射就是混合多层纹理的颜色,然后应用到物体表面。而为了处理上的方便,Direct3D将颜色的RGB通道和Alpha通道分开来进行处理,具体的操作方法就是通过纹理阶段状态(Texture Stage State)的设置。
其实也就是一个函数IDirect3DDevice9::SetTextureStageState的用法:
HRESULT SetTextureStageState(
[in] DWORD Stage,
[in] D3DTEXTURESTAGESTATETYPE Type,
[in] DWORD Value
);
■ 第一个参数,DWORD类型的Stage,指定当前设置的纹理层为第几层(有效值0~7)
■ 第二个参数,D3DTEXTURESTAGESTATETYPE类型的Type,填将要设置的纹理渲染状态,在枚举类型D3DTEXTURESTAGESTATETYPE中任意取值。先看完第三个参数,然后一起看一下这个D3DTEXTURESTAGESTATETYPE枚举类型。
■ 第三个参数,DWORD类型的Value,表示所设置的状态值,它是根据第二个参数来决定具体取什么值的。
下面就来一起看一下D3DTEXTURESTAGESTATETYPE枚举类型的定义:
typedef enum D3DTEXTURESTAGESTATETYPE {
D3DTSS_COLOROP = ,
D3DTSS_COLORARG1 = ,
D3DTSS_COLORARG2 = ,
D3DTSS_ALPHAOP = ,
D3DTSS_ALPHAARG1 = ,
D3DTSS_ALPHAARG2 = ,
D3DTSS_BUMPENVMAT00 = ,
D3DTSS_BUMPENVMAT01 = ,
D3DTSS_BUMPENVMAT10 = ,
D3DTSS_BUMPENVMAT11 = ,
D3DTSS_TEXCOORDINDEX = ,
D3DTSS_BUMPENVLSCALE = ,
D3DTSS_BUMPENVLOFFSET = ,
D3DTSS_TEXTURETRANSFORMFLAGS = ,
D3DTSS_COLORARG0 = ,
D3DTSS_ALPHAARG0 = ,
D3DTSS_RESULTARG = ,
D3DTSS_CONSTANT = ,
D3DTSS_FORCE_DWORD = 0x7fffffff
} D3DTEXTURESTAGESTATETYPE, *LPD3DTEXTURESTAGESTATETYPE;
可以看到这个枚举中的参数非常多,重点看一下前两个参数。
■ D3DTSS_COLOROP:指定纹理颜色的混合方法,对应的Value值(SetTextureStageState第三个参数)在D3DTEXTUREOP枚举类型中取值。我们把几种常用的列出来就好了。Value值取D3DTOP_DISABLE表示禁用当前纹理层颜色输出;Value值取D3DTOP_SELECTARG1或者D3DTOP_SELECTARG2,分别表示将颜色混合阶段的第一个或者第二个参数的颜色值或者alpha值输出。Value值取D3DTOP_MODULATE表示将颜色混合阶层的第一个和第二个颜色相乘并输出。
■ D3DTSS_COLORAG1:取这个值的话表示对纹理颜色混合阶段的第一个参数进行操作,而它的Value值在D3DTA常量中取值,默认值为D3DTA_TEXTURE,表示这个纹理阶段的参数就取纹理的颜色。
RenderSkyBox函数中用到的两句关于纹理阶段状态的代码:
m_pd3dDevice->SetTextureStageState( , D3DTSS_COLOROP, D3DTOP_SELECTARG1); //将纹理颜色混合的第一个参数的颜色值用于输出
m_pd3dDevice->SetTextureStageState( , D3DTSS_COLORARG1, D3DTA_TEXTURE ); //纹理颜色混合的第一个参数的值就取纹理颜色值
第一句SetTextureStageState中我们表示要将纹理颜色混合的第一个参数的颜色值用于输出,然后第二句马上就把第一个参数的颜色值取为纹理颜色值了,这样我们颜色混合后的值就是纹理的颜色值。
完整的RenderSkyBox函数:
// Name: SkyBoxClass::RenderSkyBox()
// Desc: 绘制出天空盒,可以通过第二个参数选择是否绘制出线框
//--------------------------------------------------------------------------------------
void SkyBoxClass::RenderSkyBox( D3DXMATRIX *pMatWorld, BOOL bRenderFrame )
{
m_pd3dDevice->SetTextureStageState( , D3DTSS_COLOROP, D3DTOP_SELECTARG1); //将纹理颜色混合的第一个参数的颜色值用于输出
m_pd3dDevice->SetTextureStageState( , D3DTSS_COLORARG1, D3DTA_TEXTURE ); //纹理颜色混合的第一个参数的值就取纹理颜色值
m_pd3dDevice->SetTransform( D3DTS_WORLD, pMatWorld ); //设置世界矩阵
m_pd3dDevice->SetStreamSource(,m_pVertexBuffer, , sizeof(SKYBOXVERTEX)); //把包含的几何体信息的顶点缓存和渲染流水线相关联
m_pd3dDevice->SetFVF(D3DFVF_SKYBOX); //设置FVF灵活顶点格式 //一个for循环,将5个面绘制出来
for(int i =; i<; i++)
{
m_pd3dDevice->SetTexture(, m_pTexture[i]);
m_pd3dDevice->DrawPrimitive(D3DPT_TRIANGLESTRIP, i*, );
} //对是否渲染线框的处理代码
if (bRenderFrame) //如果要渲染出线框的话
{
m_pd3dDevice->SetRenderState(D3DRS_FILLMODE, D3DFILL_WIREFRAME); //把填充模式设为线框填充
//一个for循环,将5个面的线框绘制出来
for(int i =; i<; i++)
{
m_pd3dDevice->DrawPrimitive(D3DPT_TRIANGLESTRIP, i*, ); //绘制顶点
} m_pd3dDevice->SetRenderState(D3DRS_FILLMODE, D3DFILL_SOLID); //把填充模式调回实体填充
}
}
最后就剩下了析构函数了:
// Desc: 析构函数
//-------------------------------------------------------------------------------------------------
SkyBoxClass::~SkyBoxClass(void)
{
SAFE_RELEASE( m_pVertexBuffer );
for(int i=; i<; i++)
{
SAFE_RELEASE( m_pTexture[i] );
}
}
天空盒类的使用
首先,定义一个SkyBoxClass类的指针:
SkyBoxClass* g_pSkyBox=NULL; //天空盒类的指针实例
然后,在初始化阶段拿着天空类的指针对象pSkyBox到处“指”,创建并初始化天空:
//创建并初始化天空对象
g_pSkyBox = new SkyBoxClass( g_pd3dDevice );
g_pSkyBox->LoadSkyTextureFromFile(L"GameMedia\\frontsnow1.jpg",L"GameMedia\\backsnow1.jpg",L"GameMedia\\leftsnow1.jpg",L"GameMedia\\rightsnow1.jpg", L"GameMedia\\topsnow1.jpg");//从文件加载前、后、左、右、顶面5个面的纹理图
g_pSkyBox->InitSkyBox(); //设置天空盒的边长
最后,就是在Render函数中依然是拿着天空类的指针对象pSkyBox指一下RenderSkyBox函数,进行渲染。在渲染之前需要给RenderSkyBox函数准备一个合适的世界矩阵,我们这里为了把天空盒调到适当的地方先是创建了一个平移矩阵matTransSky,然后让天空盒可以不停地缓慢移动,创建了一个随系统时间随Y轴旋转的matRotSky矩阵。接着把这两个矩阵相乘,结果等于最终的matSky矩阵,然后就可以把matSky作为参数,调用RenderSkyBox函数了。
//绘制天空
D3DXMATRIX matSky,matTransSky,matRotSky;
D3DXMatrixTranslation(&matTransSky,0.0f,-3500.0f,0.0f);
D3DXMatrixRotationY(&matRotSky, -0.000005f*timeGetTime()); //旋转天空网格, 简单模拟云彩运动效果
matSky=matTransSky*matRotSky;
g_pSkyBox->RenderSkyBox(&matSky, false);
示例代码见原博客:【Visual C++】游戏开发四十九 浅墨DirectX教程十七 三维天空的实现
第24章 三维粒子系统的实现
粒子系统的基本原理
粒子通常都是一个带有纹理的四边形。我们通过这个使用了纹理映射的四边形,可以认为粒子实际上是一个很小的网格模型,只不过是纹理赋予了它特殊的外表罢了。绘制粒子就如果绘制多边形一样简单,因为一个粒子说白了就是一个可改变大小并映射了纹理的四边形罢了。
下图就是一个20个单位大小的粒子。
如果给出了粒子中心点的坐标和粒子的大小,不难计算出绘制粒子所需要的4个顶点坐标,这样往往比直接给4个顶点坐标来得直观和节省空间。另外,很多情况下,因为一个例子是使用两个三角形组成的一个矩形来表示的,所以通常需要使粒子四边形始终面向观察者,这就用到了我们在Direct3D中的广告板(Billboard)技术,也叫公告版技术。公告版技术的基本原理就是在渲染一个多边形时,首先根据观察方向构造一个旋转矩阵,利用这个旋转矩阵旋转多边形让这个多边形始终是面向观察者的,如果观察方向是不断变化的,那么我们这个旋转矩阵也要不断进行调节。这样,我们始终看到的是这个多边形“最美好”的一面。这样先让多边形面向观察者,然后再渲染的技术,就是传说中的广告板(Billboard)技术。
粒子系统都由大量的粒子构成,每个粒子都有一组属性,如位置、大小以及纹理,还比如颜色、透明度、运动速度、加速度、自旋周期,生命周期等等属性。一个粒子需要具有什么样的属性,当然是取决于具体的运用了。
另外,粒子属性的初始值常常都是随机值,而粒子的产生也常常是由位于空间中某个位置的粒子源产生的。
粒子系统在宏观和微观上都是随时间不断变化的,一套粒子系统在它生命周期的每一刻,一般都需完成以下的四步曲的工作:
1.产生新的粒子
这一步当中,我们会根据预定的要求,产生一定数目的新粒子。粒子的各项初始属性都可以用rand函数来在一定的范围内赋上随机的值。
2.更新现有粒子的属性
比如粒子有位置和移动速度,自旋速度等等属性,这就需要在每一帧当中根据原来的粒子的位置、移动速度和自旋速度重新进行计算和赋值更新。
3.删除已经消亡的粒子
这一步是可选的,具体情况具体分析,因为有些粒子系统中粒子是一直都存在的,没有消亡一说。在规定了粒子生命周期的一套粒子系统中,就需要判断每个粒子是否生命走到了尽头,如果是的话,那么它就game over,消亡了,得用相关代码把它从粒子系统中消除。
4.绘制出粒子
在Direct3D 8.0以后,我们可以通过一种称为点精灵(Point Sprite)的特殊点元来描述粒子系统中的粒子。和一般点元不同的是,点精灵可以进行纹理映射并改变大小。点精灵的使用常常是伴随着SetRenderState中第一个参数取如下的几个值:
D3DRS_POINTSIZE = 154,
D3DRS_POINTSIZE_MIN = 155,
D3DRS_POINTSPRITEENABLE = 156,
D3DRS_POINTSCALEENABLE = 157,
D3DRS_POINTSCALE_A = 158,
D3DRS_POINTSCALE_B = 159,
D3DRS_POINTSCALE_C = 160,
另外,粒子系统中的一个重要要素是保存粒子的存储结构。我们可以用数组,如果需要动态插入和删除原始的话,就进一步使用链表用或者模板了。
需要注意的是,因为粒子系统中会有很多粒子需要不断地产生、消亡,如果在每个粒子产生时都分配内存,或者在每个粒子消亡时都释放内存,这显然会造成巨大的资源开销,非常不推荐。这里我们按链表这种方案来讲解。我们通常采用的做法是,未雨绸缪,预先为所有的粒子分配内存,并将这些粒子保存到一个链表当中。当需要产生新的粒子时,从这个链表中取出所需数量的粒子,并将它们加入到渲染链表中,而当一个粒子消亡后,重新将它们放回到原链表中,并从渲染链表中删除这些粒子。最后,在程序结束时,统一一次性释放所有粒子所占的内存空间。
雪花粒子系统的实现
1. 在雪花飞扬场景中,不需要用点精灵或者公告版技术来让粒子的四个顶点所在的面始终朝向观察者,因为雪花飞舞起来是非常优雅的,会悠扬地绕着不同的轴打转,用了公告板技术反而画蛇添足,显得不那么真实了。
2. 在雪花飞扬场景中,可以不需要粒子的动态消亡与产生,可以让雪花粒子在一定区域内下落,如果下落到Y轴的临界区域,就把粒子的Y坐标设为预定的临界最高点的Y坐标,就像粒子都是从这个地方产生的一样,这样就会模拟出源源不断地下雪景象。
首先依旧是写出这个名为SnowParticleClass类的大体轮廓。
首先写4个宏,方便宏观调控。这四个宏分别用于表示雪花粒子数量,雪花飞扬区域的长度,雪花飞扬区域的宽度,雪花飞扬区域的高度。
#define PARTICLE_NUMBER 100000 //雪花粒子数量,显卡不好、运行起来卡的的童鞋请取小一点。
#define SNOW_SYSTEM_LENGTH_X 20000 //下雪区域的长度
#define SNOW_SYSTEM_WIDTH_Z 20000 //下雪区域的宽度
#define SNOW_SYSTEM_HEIGHT_Y 20000 //下雪区域的高度
然后我们写出雪花粒子的FVF灵活顶点格式。顶点属性当然是顶点坐标加上纹理坐标了:
struct POINTVERTEX
{
floatx, y, z; //顶点位置
floatu,v ; //顶点纹理坐标
};
#define D3DFVF_POINTVERTEX(D3DFVF_XYZ|D3DFVF_TEX1)
接下来是雪花粒子的属性结构体:
struct SNOWPARTICLE
{
floatx, y, z; //坐标位置
floatRotationY; //雪花绕自身Y轴旋转角度
floatRotationX; //雪花绕自身X轴旋转角度
floatFallSpeed; //雪花下降速度
floatRotationSpeed; //雪花旋转速度
int TextureIndex; //纹理索引数
};
下面正式来设计这个类,定义头文件SnowParticleClass.h:
#pragma once
#include "D3DUtil.h"
#define PARTICLE_NUMBER 20000 //雪花粒子数量,显卡不好、运行起来卡的童鞋请取小一点。
#define SNOW_SYSTEM_LENGTH_X 20000 //下雪区域的长度
#define SNOW_SYSTEM_WIDTH_Z 20000 //下雪区域的宽度
#define SNOW_SYSTEM_HEIGHT_Y 20000 //下雪区域的高度 //-----------------------------------------------------------------------------
//点精灵顶点结构和顶点格式
//-----------------------------------------------------------------------------
struct POINTVERTEX
{
floatx, y, z; //顶点位置
floatu,v ; //顶点纹理坐标
};
#define D3DFVF_POINTVERTEX(D3DFVF_XYZ|D3DFVF_TEX1) //-----------------------------------------------------------------------------
// Desc: 雪花粒子结构体的定义
//-----------------------------------------------------------------------------
struct SNOWPARTICLE
{
floatx, y, z; //坐标位置
floatRotationY; //雪花绕自身Y轴旋转角度
floatRotationX; //雪花绕自身X轴旋转角度
floatFallSpeed; //雪花下降速度
floatRotationSpeed; //雪花旋转速度
int TextureIndex; //纹理索引数
}; //-----------------------------------------------------------------------------
// Desc: 粒子系统类的定义
//-----------------------------------------------------------------------------
class SnowParticleClass
{
private:
LPDIRECT3DDEVICE9 m_pd3dDevice; //D3D设备对象
SNOWPARTICLE m_Snows[PARTICLE_NUMBER]; //雪花粒子数组
LPDIRECT3DVERTEXBUFFER9 m_pVertexBuffer; //保存粒子数据的顶点缓存
LPDIRECT3DTEXTURE9 m_pTexture[]; //雪花纹理 public:
SnowParticleClass(LPDIRECT3DDEVICE9pd3dDevice); //构造函数
~SnowParticleClass(); //析构函数
HRESULTInitSnowParticle(); //粒子系统初始化函数
HRESULTUpdateSnowParticle( float fElapsedTime); //粒子系统更新函数
HRESULTRenderSnowParticle( ); //粒子系统渲染函数
};
雪花粒子类实现
首先是构造函数:
// Desc: 构造函数
//-------------------------------------------------------------------------------------------------
SnowParticleClass::SnowParticleClass(LPDIRECT3DDEVICE9pd3dDevice)
{
//给各个参数赋初值
m_pd3dDevice=pd3dDevice;
m_pVertexBuffer=NULL;
for(inti=; i<; i++)
m_pTexture[i]= NULL;
}
接下来,粒子系统初始化函数InitSnowParticle()。首先调用srand重新播种一下随机数种子。然后for循环为所有的雪花粒子赋予独一无二的各项属性值。接着,用顶点缓存使用五步曲的其中的三步为代表着所有雪花粒子属性的一个顶点缓存赋值,最后调用6次D3DXCreateTextureFromFile从文件加载6种不同的雪花纹理进来。这6种雪花纹理图是浅墨按照素材PS出来,分别导出的,效果图在下面:
InitSnowParticle()函数的实现代码:
// Desc: 粒子系统初始化函数
//-------------------------------------------------------------------------------------------------
HRESULTSnowParticleClass::InitSnowParticle( )
{
//初始化雪花粒子数组
srand(GetTickCount());
for(inti=; i<PARTICLE_NUMBER; i++)
{
m_Snows[i].x =float(rand()%SNOW_SYSTEM_LENGTH_X-SNOW_SYSTEM_LENGTH_X/);
m_Snows[i].z = float(rand()%SNOW_SYSTEM_WIDTH_Z-SNOW_SYSTEM_WIDTH_Z/);
m_Snows[i].y = float(rand()%SNOW_SYSTEM_HEIGHT_Y);
m_Snows[i].RotationY = (rand()%)/50.0f*D3DX_PI;
m_Snows[i].RotationX = (rand()%)/50.0f*D3DX_PI;
m_Snows[i].FallSpeed = 300.0f + rand()%;
m_Snows[i].RotationSpeed = 5.0f + rand()%/10.0f;
m_Snows[i].TextureIndex= rand()%;
} //创建雪花粒子顶点缓存
m_pd3dDevice->CreateVertexBuffer(*sizeof(POINTVERTEX), ,
D3DFVF_POINTVERTEX,D3DPOOL_MANAGED,&m_pVertexBuffer, NULL ); //填充雪花粒子顶点缓存
POINTVERTEXvertices[] =
{
{-20.0f, 0.0f, 0.0f, 0.0f, 1.0f, },
{-20.0f, 40.0f, 0.0f, 0.0f, 0.0f, },
{ 20.0f, 0.0f, 0.0f, 1.0f, 1.0f, },
{ 20.0f, 40.0f, 0.0f, 1.0f, 0.0f, }
};
//加锁
VOID*pVertices;
m_pVertexBuffer->Lock(, sizeof(vertices), (void**)&pVertices, );
//访问
memcpy(pVertices, vertices, sizeof(vertices) );
//解锁
m_pVertexBuffer->Unlock(); //创建6种雪花纹理
D3DXCreateTextureFromFile(m_pd3dDevice, L"GameMedia\\snow1.jpg", &m_pTexture[] );
D3DXCreateTextureFromFile(m_pd3dDevice, L"GameMedia\\snow2.jpg", &m_pTexture[] );
D3DXCreateTextureFromFile(m_pd3dDevice, L"GameMedia\\snow3.jpg", &m_pTexture[] );
D3DXCreateTextureFromFile(m_pd3dDevice, L"GameMedia\\snow4.jpg", &m_pTexture[] );
D3DXCreateTextureFromFile(m_pd3dDevice, L"GameMedia\\snow5.jpg", &m_pTexture[] );
D3DXCreateTextureFromFile(m_pd3dDevice, L"GameMedia\\snow6.jpg", &m_pTexture[] ); returnS_OK;
}
接着是粒子系统更新函数UpdateSnowParticle,用一个for循环遍历所有的粒子,看有哪些需要更新的就可以了,对于我们雪花粒子系统,需要更新一下每个粒子的Y坐标,然后判断是否到了“地面”,然后还要改变其自旋角度:
HRESULTSnowParticleClass::UpdateSnowParticle( float fElapsedTime)
{ //一个for循环,更新每个雪花粒子的当前位置和角度
for(inti=; i<PARTICLE_NUMBER; i++)
{
m_Snows[i].y-= m_Snows[i].FallSpeed*fElapsedTime; //如果雪花粒子落到地面, 重新将其高度设置为最大
if(m_Snows[i].y<)
m_Snows[i].y= SNOW_SYSTEM_WIDTH_Z;
//更改自旋角度
m_Snows[i].RotationY += m_Snows[i].RotationSpeed * fElapsedTime;
m_Snows[i].RotationX += m_Snows[i].RotationSpeed * fElapsedTime;
} returnS_OK;
}
最后是最关键的粒子系统渲染函数RenderSnowParticle。首先禁用照明,然后设置纹理状态,接着设置设置Alpha混合系数,设置背面消隐模式为不剔除,然后就开始渲染了。需要注意的是设置Alpha混合系数,这里理解它的功能为进行透明贴图就行了:
HRESULT SnowParticleClass::RenderSnowParticle( )
{
//禁用照明效果
m_pd3dDevice->SetRenderState(D3DRS_LIGHTING, false ); //设置纹理状态
m_pd3dDevice->SetTextureStageState(, D3DTSS_COLOROP, D3DTOP_SELECTARG1); //将纹理颜色混合的第一个参数的颜色值用于输出
m_pd3dDevice->SetTextureStageState(, D3DTSS_COLORARG1, D3DTA_TEXTURE ); //纹理颜色混合的第一个参数的值就取纹理颜色值
m_pd3dDevice->SetSamplerState(, D3DSAMP_MINFILTER, D3DTEXF_LINEAR ); //缩小过滤状态采用线性纹理过滤
m_pd3dDevice->SetSamplerState(, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR ); //放大过滤状态采用线性纹理过滤 //设置Alpha混合系数
m_pd3dDevice->SetRenderState(D3DRS_ALPHABLENDENABLE,true); //打开Alpha混合
m_pd3dDevice->SetRenderState(D3DRS_SRCBLEND,D3DBLEND_ONE); //源混合系数设为1
m_pd3dDevice->SetRenderState(D3DRS_DESTBLEND,D3DBLEND_ONE); //目标混合系数设为1 //设置剔出模式为不剔除任何面
m_pd3dDevice->SetRenderState(D3DRS_CULLMODE, D3DCULL_NONE ); //渲染雪花
for(inti=; i<PARTICLE_NUMBER; i++)
{
//构造并设置当前雪花粒子的世界矩阵
staticD3DXMATRIX matYaw, matPitch, matTrans, matWorld;
D3DXMatrixRotationY(&matYaw,m_Snows[i].RotationY);
D3DXMatrixRotationX(&matPitch,m_Snows[i].RotationX);
D3DXMatrixTranslation(&matTrans,m_Snows[i].x, m_Snows[i].y, m_Snows[i].z);
matWorld= matYaw * matPitch * matTrans;
m_pd3dDevice->SetTransform(D3DTS_WORLD, &matWorld); //渲染当前雪花粒子
m_pd3dDevice->SetTexture(, m_pTexture[m_Snows[i].TextureIndex] ); //设置纹理
m_pd3dDevice->SetStreamSource(,m_pVertexBuffer, , sizeof(POINTVERTEX)); //把包含的几何体信息的顶点缓存和渲染流水线相关联
m_pd3dDevice->SetFVF(D3DFVF_POINTVERTEX); //设置FVF灵活顶点格式
m_pd3dDevice->DrawPrimitive(D3DPT_TRIANGLESTRIP,, ); //绘制 } //恢复相关渲染状态:Alpha混合 、剔除状态、光照
m_pd3dDevice->SetRenderState(D3DRS_ALPHABLENDENABLE,false);
m_pd3dDevice->SetRenderState(D3DRS_CULLMODE, D3DCULL_CCW );
m_pd3dDevice->SetRenderState(D3DRS_LIGHTING, true ); returnS_OK;
}
最后是析构函数:
SnowParticleClass::~SnowParticleClass()
{
SAFE_RELEASE(m_pVertexBuffer); for(inti=;i<; i++)
{
SAFE_RELEASE(m_pTexture[i]);
}
雪花粒子类的使用
首先,定义一个SnowParticleClass类的全局指针实例:
SnowParticleClass* g_pSnowParticles= NULL; //雪花粒子系统的指针实例
然后,在初始化阶段拿着雪花飞扬类的指针对象SnowParticleClass到处“指”,创建并初始化粒子系统:
//创建并初始化雪花粒子系统
g_pSnowParticles= new SnowParticleClass(g_pd3dDevice);
g_pSnowParticles->InitSnowParticle();
最后,就是在Render函数中依然是拿着天空类的指针对象g_pSnowParticles先指一下UpdateSnowParticle函数,更新粒子系统,然后再指一下RenderSnowParticle函数,进行渲染。
//绘制雪花粒子系统
g_pSnowParticles->UpdateSnowParticle(fTimeDelta);
g_pSnowParticles->RenderSnowParticle();
上面更新粒子系统的UpdateSnowParticle函数我们用到了一个流逝时间参数fTimeDelta,要把消息循环中改成如下含有流逝时间的更加先进的消息循环体系,然后让Direct3D_Update和Direct3D_Render各增加一个代表流逝时间的fTimeDelta参数。
//消息循环过程
MSGmsg = { }; //初始化msg
while(msg.message != WM_QUIT ) //使用while循环
{
staticFLOAT fLastTime =(float)::timeGetTime();
staticFLOAT fCurrTime =(float)::timeGetTime();
staticFLOAT fTimeDelta = 0.0f;
fCurrTime = (float)::timeGetTime();
fTimeDelta= (fCurrTime - fLastTime) / 1000.0f;
fLastTime = fCurrTime; if(PeekMessage( &msg, , , , PM_REMOVE ) ) //查看应用程序消息队列,有消息时将队列中的消息派发出去。
{
TranslateMessage(&msg ); //将虚拟键消息转换为字符消息
DispatchMessage(&msg ); //该函数分发一个消息给窗口程序。
}
else
{
Direct3D_Update(hwnd,fTimeDelta); //调用更新函数,进行画面的更新
Direct3D_Render(hwnd,fTimeDelta); //调用渲染函数,进行画面的渲染
}
}
示例程序见原博客:【Visual C++】游戏开发五十 浅墨DirectX教程十八 雪花飞扬:实现唯美的粒子系统
第25章 多游戏模型的载入
几乎和作者博文【Visual C++】游戏开发五十一 浅墨DirectX教程十九 网格模型进阶之路相同。
读后感:整本书看得也差不多了吗,剩下的任务是将书中的代码好好敲几遍。感到比较坑爹的是书中的内容几乎和作者的博客一样,早知道我就看博客,不买书了呀。这本书还是蛮适合初学者的,但是对不了解Windows编程的人来说,可能刚开始会有点困难。