第24章 让唯美的雪花飘扬——三维粒子系统的实现

章节导读

本章我们将一起探讨三维游戏中粒子系统的方方面面, 首先对粒子系统的基本概念特性做一个全面的认知,然后依旧是把粒子系统封装在一个C++类中,模拟了三维游戏中唯美的雪花飞扬的景象, 让我们之前实现的综合三维游戏场景更加炫。

大家应该记得,我们之前也用GDI 实现过雪花粒子系统,那个时候由于图形库GDI的限制,实现效果或多或少显得有些拙劣,本章我们在DirectX 的帮助下,专门用粒子系统重新实现了唯美雪花的飞扬景象,算是为强大的粒子系统正名吧。

24.1 对粒子系统的基本认知

1983 年,奇才Reeves .V.T 在他发表的论文《Particle Systems A Technique for Modeling a Class of  Fuzzy Objects》中首次提出了粒子系统的概念。从此,粒子系统就开始广泛运用于计算机中各种模 糊景物的模拟。经常使用粒子系统模拟的现象有火焰、爆炸、烟、水流、火花、落叶、云、雾、雪、 尘、流星尾迹或者像发光轨迹这样的抽象视觉效果等等。这些物体模型在计算机中往往很难用具体 的形状、大小来描述,但是我们可以通过粒子系统的思想,去描述组成这些物体的每个元素和它的 变化。
一般情况下,粒子的几何特征都十分简单,可以采用一个像素或者一个小的多边形来表示。需 要注意的是,粒子系统的最大的缺陷是,当粒子数量达到很大的规模的时候, 对运行时机器性能的 要求会更加苛刻,如果机器的性能跟不上, 就会达不到实时的显示效果,换句话说,就是粒子太多 了,我们的电脑跑不动了, 很卡。
在许多三维建模及渲染包内部就可以创建、修改粒子系统,如3DS Max 、Maya 以及Blender 等,这些编辑程序使艺术家能够立即看到它们设定的特性或者规则下粒子系统的表现。  而2D 的粒子 特效软件中, particlelllusion 最为出色, 因为他的渲染比一般的3D 软件快较为平面化。 目前,粒子系统技术被广泛用于大型3D 游戏的制作中。

下图是DirectX SDK 中自带Sample 一个和粒子系统相关的非常华丽的demo ,推荐大家试着运行一下,如果你的DirectX SDK 安装在D 盘,那么路径就是:

D:\Program Files (x86)\Microsoft DirectX SDK (June 2010)\Samples\C++\Direct3D11\NBodyGravityCS11

放2张运行的截图:

  

上面程序中的NBody 粒子系统不是普通的粒子系统,它运用了并行计算,以模拟银河系的强大级粒子系统。普通粒子系统的粒子之间是无关联的. 而NBody 的粒子之间有引力的影响存在,这点大家需要注意。

粒子系统通常有3 个要素:群体性、统一性和随机性。下面我们简单看一下它们的定义:

  •  群体性:粒子系统是由大量的可见元素构成的。因此,用粒子系统描述一团烟雾是合情合理的,但是我们所用粒子系统去描述一粒烟雾显然只有闹笑话了。
  •  统一性:粒子系统的每个元素有具有相同的表现规律。比如,雪花粒子系统中的每一片雪花,都是白色元暇、轻盈灵动的。如果雪花粒子系统中出现了彩色的粒子,那显然就是异类了。
  • 随机性: 粒子系统中每个元素会随机表现出不同的特性。比如烟雾中每一个烟雾粒子的运动轨迹往往都是杂乱无章的,但是对于整个烟雾系统来说,这些烟雾粒子往往都有一个大体的运动方向。

24.2 粒子系统的基本原理

粒子通常都是一个带有纹理的四边形,通常是使用了纹理映射的四边形。可以认为粒子实际上 是一个很小的网格模型,只不过是纹理赋予了它特殊的外表罢了。绘制粒子就如同绘制多边形一样 简单,因为一个粒子说白了就是一个可改变大小并映射了纹理的四边形罢了。
下图所示的就是一个20 个单位大小的粒子。


如果给出了粒子中心点的坐标和粒子的大小,不难计算出绘制粒子所需要的4 个顶点坐标,这样往往比直接给4 个顶点坐标来得直观和节省空间。
另外,很多情况下,因为一个例子是使用两个三角形组成的一个矩形来表示的,所以通常需要使粒子四边形始终面向观察者,这就用到了Direct3D 中的广告板(Billboard )技术,也叫公告版技术。公告版技术的基本原理在这里也提一下吧,后面有机会就专门讲解一下。公告版技术的基本原理就是在渲染一个多边形时,首先根据观察方向构造一个旋转矩阵,利用这个旋转矩阵旋转多边形,让这个多边形始终是面向观察者的,如果观察方向是不断变化的, 那么我们这个旋转矩阵也要不断进行调节。这样,我们始终看到的是这个多边形“最美好”的一面。这种先让多边形面向观察者,然后再渲染的技术,就是传说中的广告板技术。
我们知道,粒子系统都由大量的粒子构成,每个粒子都有一组属性,如位置、大小以及纹理,还比如颜色、透明度、运动速度、加速度、自旋周期、生命周期等等属性。一个粒子需要具有什么样的属性, 当然是取决于具体的运用了。
另外,粒子属性的初始值常常都是随机值,而粒子的产生也常常是由位于空间中某个位置的粒子源产生的。
粒子系统在宏观和微观上都是随时间不断变化的, 一套粒子系统在它生命周期的每一刻, 一般都需完成以下的四步曲的工作:

1 . 产生新的粒子
在这一步中,我们会根据预定的要求, 产生一定数目的新粒子。粒子的各项初始属性都可以用rand 函数来在一定的范围内赋上随机值。
2. 更新现有粒子的属性
比如粒子有位置和移动速度, 自旋速度等等属性,这就需要在每一帧当中根据原来的粒子的位置、移动速度和自旋速度重新进行计算和赋值更新。
3. 删除已经消亡的粒子
这一步是可选的, 具体情况具体分析,因为有些粒子系统中粒子是一直都存在的,没有消亡一说。在规定了粒子生命周期的一套粒子系统中,需要判断每个粒子是否生命走到了尽头,如果是的话,那么它就消亡了,得用相关代码把它从粒子系统中消除。

4. 绘制出粒子
这步没有的话什么都不是,不显示出来叫什么粒子系统啊。人家可不管你在之前做了多少工作,算了多少东西,反正玩家是要看到最终的显示效果的。
在Direct3D 8.0 以后,我们可以通过一种称为点精灵( Point Sprite )的特殊点元来描述粒子系统中的粒子。和一般点元不同的是,点精灵可以进行纹理映射并改变大小。点精灵的使用常常是伴随着SetRenderState 中第一个参数取如下的几个值:

 D3DRS POINTS I ZE = 154 ,
 D3DRS POINTSIZE MIN = 155,
 D3DRS POINTSPRITEENABLE = 156,
 D3DRS POINTSCALEENABLE = 157 ,
 D3DRS POINTSCALE_A = 158 ,
 D3DRS POINTSCALE_B = 159,
 D3DRS POINTSCALE_C = 160 ,
另外,粒子系统中的一个重要要素是保存粒子的存储结构。我们可以使用数组,如果需要动态 插入和删除原始的话,就进一步使用链表或者模板了。

需要注意的是,因为粒子系统中会有很多粒子需要不断地产生、消亡,如果在每个粒子产生时都分配内存,或者在每个粒子消亡时都释放内存,这显然会造成巨大的资源开销,非常不推荐。这里我们按链表这种方案来讲解。通常采用的做法是,未雨绸缪,预先为所有的粒子分配内存,并将这些粒子保存到一个链表当中。当需要产生新的粒子时,从这个链表中取出所需数量的粒子,并将
它们加入到渲染链表中,而当一个粒子消亡后,重新将它们放回到原链表中,并从渲染链表中删除这些粒子。最后,在程序结束时,一次性释放所有粒子所占的内存空间。这就是比较科学的做法。

24.3 雪花粒子系统的设计

我们之前己经提到过,粒子系统可以模拟很多的现象,比如火焰、爆炸、烟、水流、火花、落叶、云、雾、雪、尘、流星尾迹或者发光轨迹。对于现象的模拟,粒子的特性往往需要根据模拟的现象的属性来具体地设计。
对于本节需要使用粒子系统来模拟的雪花飞扬场景,有两个比较特殊的地方:
  •  在雪花飞扬场景中,不需要用点精灵或者公告版技术来让粒子的4 个顶点所在的面始终朝向观察者,因为雪花飞舞起来是非常优雅的,会悠扬地绕着不同的轴打转,用了公告板技术反而画蛇添足,显得不那么真实了。
  •  在雪花飞扬场景中,不需要粒子的动态消亡与产生,可以让雪花粒子在一定区域内下落,如果下落到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   //雪花飞扬区域的高度
这里的PARTICLE_NUMBER 雪花粒子数量我们取了10 万,那么后面我们写出来的游戏场景中就有10 万个雪花粒子, 这个前提是你的显卡禁得住10 万及以上的粒子数量。当然,你取20 万的粒子数量,在下雪区域是20000 × 20000 × 20000 的区域中就是超级大暴雪了……建议这时候把长度和宽度调大一些,来让这20 万的粒子的活动区域更大。
接下来我们写出雪花粒子的FVF 灵活顶点格式,顶点属性是顶点坐标加上纹理坐标了:
//-------------------------------------------------------------------------------------------------
//雪花粒子的FVF顶点结构和顶点格式
//-------------------------------------------------------------------------------------------------
struct POINTVERTEX
{
	float x, y, z;    //顶点位置
	float u,v ;		  //顶点纹理坐标
};
#define D3DFVF_POINTVERTEX (D3DFVF_XYZ|D3DFVF_TEX1)
再接下来是雪花粒子的属性结构体,想一想现实生活中的雪花有哪些特定的属性呢?唯美的 雪花,有特定的位置, 会旋转,有下降速度,样子不同,嗯,好,那我们就这样写:
//-------------------------------------------------------------------------------------------------
// Desc: 雪花粒子结构体的定义
//-------------------------------------------------------------------------------------------------
struct  SNOWPARTICLE
{
	float x, y, z;      //坐标位置
	float RotationY;         //雪花绕自身Y轴旋转角度
	float RotationX;       //雪花绕自身X轴旋转角度
	float FallSpeed;       //雪花下降速度
	float RotationSpeed;       //雪花旋转速度
	int   TextureIndex;     //纹理索引数
};
好,边角工作完成了,下面正式来设计这个类吧。首先来看一看需要哪些成员变量,LPDIRECT3DDEVICE9 类型的设备接口指针m_pd3dDevice 不能少吧,雪花粒子数组m_Snows 要有吧,顶点缓存对象m_pVertexBuffer 要有吧,保存不同雪花纹理样式的雪花纹理数组m_pTexture要有吧,嗯,成员变量就这些。
然后看看要有哪些成员函数,构造函数、析构函数先显式地写出来,再是粒子系统初始化函数lnitSnowParticle,粒子系统更新函数UpdateSnowParticle , 粒子系统渲染函数RenderSnowParticle,嗯,成员函数也就是这些了。整体来看,这个类的轮廓就是下面SnowParticleClass.h 的全部代码:
//=============================================================================
// Name: SnowParticleClass.h
//	Des: 一个封装了雪花粒子系统系统的类的头文件
// 2013年 3月31日  Create by 浅墨 
//=============================================================================


#pragma once
#include "D3DUtil.h"
#define  PARTICLE_NUMBER  15000   //雪花粒子数量,显卡不好、运行起来卡的童鞋请取小一点。
#define  SNOW_SYSTEM_LENGTH_X	20000   //雪花飞扬区域的长度
#define  SNOW_SYSTEM_WIDTH_Z		20000   //雪花飞扬区域的宽度
#define  SNOW_SYSTEM_HEIGHT_Y     20000   //雪花飞扬区域的高度

//-------------------------------------------------------------------------------------------------
//雪花粒子的FVF顶点结构和顶点格式
//-------------------------------------------------------------------------------------------------
struct POINTVERTEX
{
	float x, y, z;    //顶点位置
	float u,v ;		  //顶点纹理坐标
};
#define D3DFVF_POINTVERTEX (D3DFVF_XYZ|D3DFVF_TEX1)


//-------------------------------------------------------------------------------------------------
// Desc: 雪花粒子结构体的定义
//-------------------------------------------------------------------------------------------------
struct  SNOWPARTICLE
{
	float x, y, z;      //坐标位置
	float RotationY;         //雪花绕自身Y轴旋转角度
	float RotationX;       //雪花绕自身X轴旋转角度
	float FallSpeed;       //雪花下降速度
	float RotationSpeed;       //雪花旋转速度
	int   TextureIndex;     //纹理索引数
};

//-------------------------------------------------------------------------------------------------
// Desc: 粒子系统类的定义
//-------------------------------------------------------------------------------------------------
class SnowParticleClass
{
private:
	LPDIRECT3DDEVICE9				m_pd3dDevice;			//D3D设备对象
	SNOWPARTICLE						m_Snows[PARTICLE_NUMBER];    //雪花粒子数组
	LPDIRECT3DVERTEXBUFFER9   m_pVertexBuffer;      //粒子顶点缓存
	LPDIRECT3DTEXTURE9			m_pTexture[6];  //雪花纹理数组

public:
	SnowParticleClass(LPDIRECT3DDEVICE9 pd3dDevice);   //构造函数
	~SnowParticleClass();					//析构函数
	HRESULT InitSnowParticle();        //粒子系统初始化函数
	HRESULT UpdateSnowParticle( float fElapsedTime);    //粒子系统更新函数
	HRESULT RenderSnowParticle( );   //粒子系统渲染函数
};

24.4 雪花粒子系统的实现

又到了做填空题的时候,对着上面我们勾勒出来的SnowParticleClass 类,有5 个函数需要填上实现代码:
首先呢,构造函数:
//-------------------------------------------------------------------------------------------------
// Desc: 构造函数
//-------------------------------------------------------------------------------------------------
SnowParticleClass::SnowParticleClass(LPDIRECT3DDEVICE9 pd3dDevice)
{
	//给各个参数赋初值
	m_pd3dDevice=pd3dDevice;
	m_pVertexBuffer=NULL;	
	for(int i=0; i<5; i++)
		m_pTexture[i] = NULL; 
}
接下来, 粒子系统初始化函数lnitSnowParticle() 。首先, 调用srand 重新播种一下随机数种子。然后for 循环为所有的雪花粒子赋予独一无二的各项属性值。接着,用讲烂了的顶点缓存使用五步曲的其中三步为代表着所有雪花粒子属性的一个顶点缓存赋值,最后调用6 次D3DXCreateTextureFromFile 从文件加载6 种不同的雪花纹理边来。这6 种雪花纹理图是按照素材PS 出来,分别导出的,效果图在下面,它们还不错,各有特点, 非常漂亮:

按照上面的思考, InitSnowParticle()函数的实现代码我们就知道怎么写了:
//-------------------------------------------------------------------------------------------------
// Name:  SnowParticleClass::InitSnowParticle( )
// Desc: 粒子系统初始化函数
//-------------------------------------------------------------------------------------------------
HRESULT SnowParticleClass::InitSnowParticle( )
{
	//初始化雪花粒子数组
	srand(GetTickCount());
	for(int i=0; i<PARTICLE_NUMBER; i++)
	{	
		m_Snows[i].x        = float(rand()%SNOW_SYSTEM_LENGTH_X-SNOW_SYSTEM_LENGTH_X/2);
		m_Snows[i].z        = float(rand()%SNOW_SYSTEM_WIDTH_Z-SNOW_SYSTEM_WIDTH_Z/2);
		m_Snows[i].y        = float(rand()%SNOW_SYSTEM_HEIGHT_Y);
		m_Snows[i].RotationY     = (rand()%100)/50.0f*D3DX_PI;
		m_Snows[i].RotationX   = (rand()%100)/50.0f*D3DX_PI;
		m_Snows[i].FallSpeed   = 300.0f + rand()%500;
		m_Snows[i].RotationSpeed   = 5.0f +  rand()%10/10.0f;
		m_Snows[i].TextureIndex = rand()%6;
	}


	//创建雪花粒子顶点缓存
	m_pd3dDevice->CreateVertexBuffer( 4*sizeof(POINTVERTEX), 0, 
		D3DFVF_POINTVERTEX,D3DPOOL_MANAGED, &m_pVertexBuffer, NULL );

	//填充雪花粒子顶点缓存
	POINTVERTEX vertices[] =
	{
		{ -30.0f, 0.0f, 0.0f,   0.0f, 1.0f, },
		{ -30.0f, 60.0f, 0.0f,   0.0f, 0.0f, },
		{  30.0f, 0.0f, 0.0f,   1.0f, 1.0f, }, 
		{  30.0f, 60.0f, 0.0f,   1.0f, 0.0f, }
	};
	//加锁
	VOID* pVertices;
	m_pVertexBuffer->Lock( 0, sizeof(vertices), (void**)&pVertices, 0 );
	//访问
	memcpy( pVertices, vertices, sizeof(vertices) );
	//解锁
	m_pVertexBuffer->Unlock();

	//创建6种雪花纹理
	D3DXCreateTextureFromFile( m_pd3dDevice, L"GameMedia\\snow1.jpg", &m_pTexture[0] );
	D3DXCreateTextureFromFile( m_pd3dDevice, L"GameMedia\\snow2.jpg", &m_pTexture[1] );
	D3DXCreateTextureFromFile( m_pd3dDevice, L"GameMedia\\snow3.jpg", &m_pTexture[2] );
	D3DXCreateTextureFromFile( m_pd3dDevice, L"GameMedia\\snow4.jpg", &m_pTexture[3] );
	D3DXCreateTextureFromFile( m_pd3dDevice, L"GameMedia\\snow5.jpg", &m_pTexture[4] );
	D3DXCreateTextureFromFile( m_pd3dDevice, L"GameMedia\\snow6.jpg", &m_pTexture[5] );

	return S_OK;
}
接着我们来看一下粒子系统更新函数UpdateSnowParticle 怎么实现。其实非常简单,就是一 个for 循环遍历所有的粒子,看有哪些需要更新的就可以了。对于这个雪花粒子系统, 需要更新 一下每个粒子的Y 坐标, 判断是否到了“地面”,然后还要改变其自旋角度。据此,代码写出来 就是这样了:
//-------------------------------------------------------------------------------------------------
// Name:  SnowParticleClass::UpdateSnowParticle( )
// Desc: 粒子系统更新函数
//-------------------------------------------------------------------------------------------------
HRESULT SnowParticleClass::UpdateSnowParticle( float fElapsedTime)
{

	//一个for循环,更新每个雪花粒子的当前位置和角度
	for(int i=0; i<PARTICLE_NUMBER; i++)
	{
		m_Snows[i].y -= m_Snows[i].FallSpeed*fElapsedTime;

		//如果雪花粒子落到地面, 重新将其高度设置为最大
		if(m_Snows[i].y<0)
			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;
	}

	return S_OK;
}
最后来看一下最关键的粒子系统渲染函数RenderSnowParticle 怎么写。首先禁用照明,然后 设置纹理状态,接着设置Alpha 混合系数,设置背面消隐模式为不剔除,最后就开始渲染。需要 注意的是,设置Alpha 混合系数,之前没有专门讲解过,这里简单理解它的功能为进行透明贴图 就行了,就是我们在写GDI 游戏小程序的时候一直在做的纠结事情,把图片背景的黑边去掉。好 了,思路有了,写代码还会难吗?
//-------------------------------------------------------------------------------------------------
// Name:  SnowParticleClass::RenderSnowParticle( )
// Desc: 粒子系统渲染函数
//-------------------------------------------------------------------------------------------------
HRESULT SnowParticleClass::RenderSnowParticle(  )
{
	//禁用照明效果
	m_pd3dDevice->SetRenderState( D3DRS_LIGHTING, false );

	//设置纹理状态
	m_pd3dDevice->SetTextureStageState( 0, D3DTSS_COLOROP,   D3DTOP_SELECTARG1);   //将纹理颜色混合的第一个参数的颜色值用于输出
	m_pd3dDevice->SetTextureStageState( 0, D3DTSS_COLORARG1, D3DTA_TEXTURE );   //纹理颜色混合的第一个参数的值就取纹理颜色值
	m_pd3dDevice->SetSamplerState( 0, D3DSAMP_MINFILTER, D3DTEXF_LINEAR );  //缩小过滤状态采用线性纹理过滤
	m_pd3dDevice->SetSamplerState( 0, 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(int i=0; i<PARTICLE_NUMBER; i++)
	{
		//构造并设置当前雪花粒子的世界矩阵
		static D3DXMATRIX 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( 0, m_pTexture[m_Snows[i].TextureIndex] );    //设置纹理
		m_pd3dDevice->SetStreamSource(0, m_pVertexBuffer, 0, sizeof(POINTVERTEX));  //把包含的几何体信息的顶点缓存和渲染流水线相关联  
		m_pd3dDevice->SetFVF(D3DFVF_POINTVERTEX);    //设置FVF灵活顶点格式
		m_pd3dDevice->DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2);   //绘制

	}

	//恢复相关渲染状态:Alpha混合 、剔除状态、光照
	m_pd3dDevice->SetRenderState(D3DRS_ALPHABLENDENABLE, false);
	m_pd3dDevice->SetRenderState( D3DRS_CULLMODE, D3DCULL_CCW );
	m_pd3dDevice->SetRenderState( D3DRS_LIGHTING, true );

	return S_OK;
}
在写渲染代码的时候,如果绘制的物体很多,那么我们需要养成好习惯,状态设了之后再渲染, 渲染完成之后要还原渲染之前时的状态。需要绘制几部分内容,就这样做几次。I

析构函数就很简单了,就是收拾残局,看看有什么COM 接口要释放的:

//-------------------------------------------------------------------------------------------------
// Desc: 析构函数
//-------------------------------------------------------------------------------------------------
SnowParticleClass::~SnowParticleClass()
{
	SAFE_RELEASE(m_pVertexBuffer);

	for(int i=0;i<3; i++)
	{
		SAFE_RELEASE(m_pTexture[i]);
	}
}

24.5 雪花飞扬粒子类的使用

与类打交道封装功能和我们经历的生活一样,也是一个先苦后甜的过程。
设计和实现这个类的时候或许是苦涩的,但是先苦后甜是必须的,写完这个类之后,用起来非常地方便,只用几行代码, 一个唯美的雪花飞扬景象就加入到我们的游戏场景中了。
也就是如下的3 步:
1:首先,定义一个SnowParticleClass 类的全局指针实例:

SnowParticleClass*    g_pSnowParticles = NULL;		//雪花粒子系统的指针实例
2:然后,在初始化阶段拿着雪花飞扬类的指针对象SnowParticleClass 到处“指”,创建并初始化粒子系统:

	//创建并初始化雪花粒子系统
	g_pSnowParticles = new SnowParticleClass(g_pd3dDevice);
	g_pSnowParticles->InitSnowParticle();
3:最后,就是在Render 函数中依然是拿着雪花类的指针对象g_pSnowParticles 先指一下UpdateSnowParticle 函数,更新粒子系统,然后再指一下RenderSnowParticle 函数,进行渲染。

	//绘制雪花粒子系统
	g_pSnowParticles->UpdateSnowParticle(fTimeDelta);
	g_pSnowParticles->RenderSnowParticle();
另外,需要注意上面更新粒子系统的UpdateSnowParticle 函数,我们用到了一个流逝时间参数fTimeDelta ,所以就需要把我们服役多年的消息循环改成如下含有流逝时间的更加先进的消息循环体系,然后让Direct3D_Update 和Direct3D_Render 各增加一个代表流逝时间的fTimeDelta 参数。

//消息循环过程
	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);			//调用渲染函数,进行画面的渲染			
		}
	}

24.6 示例程序D3Ddemo19

本节示例程序在之前的基础上又增加了两个文件,也就是实现雪花飞扬粒子系统类的源文件和 头文件。全部文件数量增加到了12 个,它们的列表如下图所示。


我们依旧只贴出最能体现思想的、提纲辈领的main.cpp 中的核心代码,首先依然是我们的老朋友Object_Init()函数:

//-----------------------------------【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"angle.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)


	//创建柱子
	D3DXCreateCylinder(g_pd3dDevice, 280.0f, 10.0f, 3000.0f, 60, 60,  &g_cylinder, 0);
	g_MaterialCylinder.Ambient  = D3DXCOLOR(1.0f, 0.0f, 0.0f, 1.0f);  
	g_MaterialCylinder.Diffuse  = D3DXCOLOR(1.0f, 0.0f, 0.0f, 1.0f);  
	g_MaterialCylinder.Specular = D3DXCOLOR(0.5f, 0.0f, 0.3f, 0.3f);  
	g_MaterialCylinder.Emissive = D3DXCOLOR(0.0f, 0.0f, 0.0f, 1.0f);

	// 设置光照  
	::ZeroMemory(&g_Light, sizeof(g_Light));  
	g_Light.Type          = D3DLIGHT_DIRECTIONAL;  
	g_Light.Ambient       = D3DXCOLOR(0.7f, 0.7f, 0.7f, 1.0f);  
	g_Light.Diffuse       = D3DXCOLOR(1.0f, 1.0f, 1.0f, 1.0f);  
	g_Light.Specular      = D3DXCOLOR(0.9f, 0.9f, 0.9f, 1.0f);  
	g_Light.Direction     = D3DXVECTOR3(1.0f, 1.0f, 1.0f);  
	g_pd3dDevice->SetLight(0, &g_Light);  
	g_pd3dDevice->LightEnable(0, true);  
	g_pd3dDevice->SetRenderState(D3DRS_NORMALIZENORMALS, true);  
	g_pd3dDevice->SetRenderState(D3DRS_SPECULARENABLE, true);

	// 创建并初始化虚拟摄像机
	g_pCamera = new CameraClass(g_pd3dDevice);
	g_pCamera->SetCameraPosition(&D3DXVECTOR3(0.0f, 1400.0f, -1800.0f));  //设置摄像机所在的位置
	g_pCamera->SetTargetPosition(&D3DXVECTOR3(0.0f, 1200.0f, 0.0f));  //设置目标观察点所在的位置
	g_pCamera->SetViewMatrix();  //设置取景变换矩阵
	g_pCamera->SetProjMatrix();  //设置投影变换矩阵

	// 创建并初始化地形
	g_pTerrain = new TerrainClass(g_pd3dDevice);   
	g_pTerrain->LoadTerrainFromFile(L"GameMedia\\heighmap.raw", L"GameMedia\\terrainstone.jpg");		//从文件加载高度图和纹理
	g_pTerrain->InitTerrain(200, 200, 60.0f, 8.0f);  //四个值分别是顶点行数,顶点列数,顶点间间距,缩放系数

	//创建并初始化天空对象
	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();


	return S_OK;
}
然后是我们的另外一个好朋友,Direct3D_Render()函数:

//-----------------------------------【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, 255, 255), 1.0f, 0);

	//--------------------------------------------------------------------------------------
	// 【Direct3D渲染五步曲之二】:开始绘制
	//--------------------------------------------------------------------------------------
	g_pd3dDevice->BeginScene();                     // 开始绘制
	
	//--------------------------------------------------------------------------------------
	// 【Direct3D渲染五步曲之三】:正式绘制
	//--------------------------------------------------------------------------------------
	//绘制人物
	D3DXMATRIX mScal,mRot2,mTrans,mFinal;   //定义一些矩阵,准备对大黄蜂进行矩阵变换
	D3DXMatrixTranslation(&mTrans,50.0f,1200.0f,0.0f);
	D3DXMatrixScaling(&mScal,3.0f,3.0f,3.0f);
	mFinal=mScal*mTrans*g_matWorld;
	g_pd3dDevice->SetTransform(D3DTS_WORLD, &mFinal);//设置模型的世界矩阵,为绘制做准备
	// 用一个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 TransMatrix, RotMatrix, FinalMatrix;
	D3DXMatrixRotationX(&RotMatrix, -D3DX_PI * 0.5f);
	g_pd3dDevice->SetLight(0, &g_Light);  
	g_pd3dDevice->SetMaterial(&g_MaterialCylinder);
	g_pd3dDevice->SetTexture(0, NULL);//设置此部分的纹理
	for(int i = 0; i < 4; i++)
	{
		D3DXMatrixTranslation(&TransMatrix, -300.0f, 0.0f, -350.0f + (i * 500.0f));
		FinalMatrix = RotMatrix * TransMatrix ;
		g_pd3dDevice->SetTransform(D3DTS_WORLD, &FinalMatrix);
		g_cylinder->DrawSubset(0);

		D3DXMatrixTranslation(&TransMatrix, 300.0f, 0.0f, -350.0f + (i * 500.0f));
		FinalMatrix = RotMatrix * TransMatrix ;
		g_pd3dDevice->SetTransform(D3DTS_WORLD, &FinalMatrix);
		g_cylinder->DrawSubset(0);
	}


	//绘制地形
	g_pTerrain->RenderTerrain(&g_matWorld, false);  //渲染地形,且第二个参数设为false,表示不渲染出地形的线框

	//绘制天空
	D3DXMATRIX matSky,matTransSky,matRotSky;
	D3DXMatrixTranslation(&matTransSky,0.0f,-12000.0f,0.0f);
	D3DXMatrixRotationY(&matRotSky, -0.000005f*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);  // 翻转与显示
	 
}
到目前为止,我们用所学的知识写出来的游戏场景程序已经比较炫了, 一些运行截图如下:



24.7 章节小憩

我们可以发现,其实Direct3D 的固定功能流水线学到目前为止,基础知识也就那么多,顶点缓存、索引缓存、四大变换、纹理映射、网格、模板缓存等等,把它们其中的几个合理地组合在一起运用一下就是新的知识,这就衍生出了我们最近三章中讲到的地形、天空、粒子系统等等知识。



  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值