三维天空的实现
二、三维天空技术阐述
回到正题来吧,讲今天的主题三维天空的实现。
不要看游戏世界中的天空好像是无边无际的,其实我们都被骗了。
在计算机的三维世界中,三维天空的绘制肯定不可能像现实生活中的天空一样,一望无际绵延无尽,往往是通过一种假象来现实的。这种假象与古代人所说的“天圆地方”有着异曲同工之妙。反正就是一个足够大的容器一样的东西把我们罩在里面,让我们像井底之蛙一样以为这就是整个世界,世界就这么大,天空就这么大。而这个足以罩住我们所置身的游戏世界的容器,可以是一个立方体,也可以是半球,甚至是一个足够大的平面。
目前描述三维天空的技术主要包括三种类型:
1.平面型天空(Sky Plane),仅用一个平面放到玩家头顶。这种方案太弱了,太容易被玩家们看穿,真实感太低,技术含量也太低。但是对于并不太注意远景的场景,用天空平面也不失为一种办法。在这种情况下,用纯色的雾来覆盖整个远景,使得远处充满神秘,遮一下羞也效果凑合。
2.天空穹庐(Sky Dome),放到玩家头顶上的是一个曲面,通常都会为一个半球。就像这样:
这种方案其实真实性最强,但是不是目前使用最广泛的方案,它涉及到天空无缝衔接的素材匮乏等的问题。
3.天空盒(Sky Box),即放到场景的是一个立方体。它是目前使用最广泛的三维天空模拟技术,网络上素材丰富,所以这次就用教大家用天空盒来模拟三维天空。天空盒经常是由24个顶点、六个面组成的立方体(或者直接从做好的X模型文件载入天空盒),并经常会随着视点的移动而移动,来刻画极远处玩家无法达到位置的天空。
天空盒对于我们来说并不是困难的事情,但是真正要在游戏中使得天空“好看”,那么,还需要有着漂亮的天空纹理素材图,可以在网上搜罗(下文有讲如何搜索),也可以拜托给美工童鞋们。
另外,在高级一些的应用中,天空盒的纹理可能同时会用来生成Cube Map,并用之来做水面倒影、云影、反光等很眩的特效,大家先有一个这方面的概念就好。
三. 天空盒的设计
本篇文章的核心知识登场。
1,准备天空盒纹理素材
天空盒的纹理自然就是我们这个天空盒子立方体每个面的纹理了,至少5个面,最多6个面,因为底面处是我们所在的土地,是地形,也就不用渲染为天空了。
这5个面可以分别单独成文件,像这样:
这5张纹理需要满足的条件是:按照规定的几个面拼接起来,能构成一幅360度并包含顶部的无缝衔接的全景图:
另外,有些游戏引擎设定了需要把5个面按某种方式连起来和成一幅图来使用,就像这样的天空盒素材:
互联网上关于天空盒的纹理素材资源很丰富,大家google/百度就可以找打很多资源的下载点。
建议用关键 skybox texture或者skybox download来google。
另外,如果想原创天空盒纹理的话,可以用DirectX SDK 中自带的DirectX Texture tool工具完成。
2 天空盒类的设计
好了,开始我们的本职工作,写代码吧。
我们今天的任务是写一个封装了天空盒渲染功能的类,我们给这个类取名为SkyBoxClass。
我们来看下这个类中有哪些内容。
最开始国际惯例,LPDIRECT3DDEVICE9类型的设备接口指针m_pd3dDevice自然不能少。
然后这个类中需要处理24个带纹理坐标的顶点来构成一个立方体盒子,自然少不了FVF灵活顶点格式和一个DIRECT3DVERTEXBUFFER接口的指针。
接着还要有五个纹理对象,分别储存5个面上的纹理图,所以一个LPDIRECT3DTEXTURE9类型的m_pTexture[5]自然也少不了。最后,还需要定义一个float类型的m_Length表示天空盒的边长。结构体和成员变量就是这些了,我们再来看一下需要有哪些成员函数。
首先构造函数析构函数我们写出来,接着再写三个函数就够了,它们分别是初始化天空盒顶点的InitSkyBox函数,加载纹理的LoadSkyTextureFromFile函数,渲染天空盒的RenderSkyBox函数。
SkyBoxClass类的轮廓就是这样了,那么把上面我们的思路实现成代码就是如下,即贴出SkyBoxClass.h中全部代码:
-
//=
-
// Name: SkyBoxClass.h
-
// Des: 一个封装了三维天空盒系统的类的头文件
-
// 2013年 3月24日 Create by 浅墨
-
//=
-
#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];
//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 );
//渲染天空盒,根据第一个参数设定天空盒世界矩阵,第二个参数选择是否渲染出线框
-
-
};
三、 天空盒类的实现
类的框架勾勒出来了,接下来就很简单,分别在类的cpp文件中实现类成员函数就好了。
Ⅰ.构造函数的实现
首先是类构造函数,蛮简单,直接对着看类定义中有哪些变量,分别赋初值就行。除了Direct3D设备对象赋值成通过函数形参传进来的设备对象指针pDevice之外,其他的参数根据类型统统取NULL或者0.0f:
-
//-------------------------------------------------------------------------------------------------
-
// Desc: 构造函数
-
//-------------------------------------------------------------------------------------------------
-
SkyBoxClass::SkyBoxClass( LPDIRECT3DDEVICE9 pDevice )
-
{
-
//给各个参数赋初值
-
m_pVertexBuffer=
NULL;
-
m_pd3dDevice=pDevice;
-
for(
int i=
0; i<
5; i++)
-
m_pTexture[i] =
NULL;
-
m_Length =
0.0f;
-
}
Ⅱ. 顶点初始化函数InitSkyBox的实现
接下来要实现的就是最关键的顶点初始化函数InitSkyBox。首先,通过形参把天空盒的边长传给代表边长的成员函数m_Length。接着就是我们熟悉的顶点缓存使用四步曲的二、三两步——创建顶点缓存、访问顶点缓存了。
我们在纹理映射第一讲中就给出了立方体表面贴纹理的24个顶点需要怎么写,我们这里的思路基本和之前讲的相同,而与D3D实现的普通立方体贴图不同的一点是,大部分情况下我们视点都包容在天空盒内部,因此,天空盒的顶点顺序应当是正好与我们之前讲的普通立方体的顶点顺序相反。所以,InitSkyBox函数的实现代码就是这样:
-
//-------------------------------------------------------------------------------------------------
-
// Name: SkyBoxClass::InitSkyBox( )
-
// Desc: 天空盒初始化函数,顶点缓冲区的赋值
-
//-------------------------------------------------------------------------------------------------
-
BOOL SkyBoxClass::InitSkyBox(
float Length )
-
{
-
m_Length=Length;
-
-
//1.创建。创建顶点缓存
-
m_pd3dDevice->CreateVertexBuffer(
20 *
sizeof(SKYBOXVERTEX),
0,
-
D3DFVF_SKYBOX, D3DPOOL_MANAGED, &m_pVertexBuffer,
0 );
-
-
//用一个结构体把顶点数据先准备好
-
SKYBOXVERTEX vertices[] =
-
{
-
//前面的四个顶点
-
{ -m_Length/
2,
0.0f, m_Length/
2,
0.0f,
1.0f, },
-
{ -m_Length/
2, m_Length/
2, m_Length/
2,
0.0f,
0.0f, },
-
{ m_Length/
2,
0.0f, m_Length/
2,
1.0f,
1.0f, },
-
{ m_Length/
2, m_Length/
2, m_Length/
2,
1.0f,
0.0f, },
-
-
//背面的四个顶点
-
{ m_Length/
2,
0.0f, -m_Length/
2,
0.0f,
1.0f, },
-
{ m_Length/
2, m_Length/
2, -m_Length/
2,
0.0f,
0.0f, },
-
{ -m_Length/
2,
0.0f, -m_Length/
2,
1.0f,
1.0f, },
-
{ -m_Length/
2, m_Length/
2, -m_Length/
2,
1.0f,
0.0f, },
-
-
//左面的四个顶点
-
{ -m_Length/
2,
0.0f, -m_Length/
2,
0.0f,
1.0f, },
-
{ -m_Length/
2, m_Length/
2, -m_Length/
2,
0.0f,
0.0f, },
-
{ -m_Length/
2,
0.0f, m_Length/
2,
1.0f,
1.0f, },
-
{ -m_Length/
2, m_Length/
2, m_Length/
2,
1.0f,
0.0f, },
-
-
//右面的四个顶点
-
{ m_Length/
2,
0.0f, m_Length/
2,
0.0f,
1.0f, },
-
{ m_Length/
2, m_Length/
2, m_Length/
2,
0.0f,
0.0f, },
-
{ m_Length/
2,
0.0f, -m_Length/
2,
1.0f,
1.0f, },
-
{ m_Length/
2, m_Length/
2, -m_Length/
2,
1.0f,
0.0f, },
-
-
//上面的四个顶点
-
{ m_Length/
2, m_Length/
2, -m_Length/
2,
1.0f,
0.0f, },
-
{ m_Length/
2, m_Length/
2, m_Length/
2,
1.0f,
1.0f, },
-
{ -m_Length/
2, m_Length/
2, -m_Length/
2,
0.0f,
0.0f, },
-
{ -m_Length/
2, m_Length/
2, m_Length/
2,
0.0f,
1.0f, },
-
-
};
-
-
//准备填充顶点数据
-
void pVertices;
-
//2.加锁
-
m_pVertexBuffer->Lock(
0,
0, (
void**)&pVertices,
0 );
-
//3.访问。把结构体中的数据直接拷到顶点缓冲区中
-
memcpy( pVertices, vertices,
sizeof(vertices) );
-
//4.解锁
-
m_pVertexBuffer->Unlock();
-
-
return TRUE;
-
}
Ⅲ.纹理载入函数LoadSkyTextureFromFile的写法
接下来看看纹理载入函数LoadSkyTextureFromFile的写法,实在是非常非常简单。
给5个文件路径给他,传进来调用5次D3DXCreateTextureFromFile函数载入纹理到m_pTexture[]数组中就好了:
-
//-------------------------------------------------------------------------------------------------
-
// Name: SkyBoxClass::LoadSkyTextureFromFile( )
-
// Desc: 天空盒纹理加载函数
-
//-------------------------------------------------------------------------------------------------
-
BOOL SkyBoxClass::LoadSkyTextureFromFile(
wchar_t *pFrontTextureFile,
wchar_t *pBackTextureFile,
wchar_t *pLeftTextureFile,
wchar_t *pRightTextureFile,
wchar_t
pTopTextureFile)
-
{
-
//从文件加载五张纹理
-
D3DXCreateTextureFromFile( m_pd3dDevice , pFrontTextureFile, &m_pTexture[
0] );
//前面
-
D3DXCreateTextureFromFile( m_pd3dDevice , pBackTextureFile, &m_pTexture[
1] );
//后面
-
D3DXCreateTextureFromFile( m_pd3dDevice , pLeftTextureFile, &m_pTexture[
2] );
//左面
-
D3DXCreateTextureFromFile( m_pd3dDevice , pRightTextureFile, &m_pTexture[
3] );
//右面
-
D3DXCreateTextureFromFile( m_pd3dDevice , pTopTextureFile, &m_pTexture[
4] );
//上面
-
return TRUE;
-
}
Ⅳ. 渲染函数RenderSkyBox
再看看作用为渲染天空盒RenderSkyBox函数。其中我们用到了讲解纹理映射的时候没有讲到的纹理阶段混合操作,这里我们顺便讲一下。
纹理映射的本质实际上就是从纹理中获取颜色值,然后应用到物体表面上。而以后我们会接触到的多次纹理映射就是混合多层纹理的颜色,然后应用到物体表面。而为了处理上的方便,Direct3D将颜色的RGB通道和Alpha通道分开来进行处理,具体的操作方法就是通过纹理阶段状态(Texture Stage State)的设置。
其实也就是一个函数IDirect3DDevice9::SetTextureStageState的用法,在MSDN中查到这个函数原型如下:
-
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 =
1,
-
D3DTSS_COLORARG1 =
2,
-
D3DTSS_COLORARG2 =
3,
-
D3DTSS_ALPHAOP =
4,
-
D3DTSS_ALPHAARG1 =
5,
-
D3DTSS_ALPHAARG2 =
6,
-
D3DTSS_BUMPENVMAT00 =
7,
-
D3DTSS_BUMPENVMAT01 =
8,
-
D3DTSS_BUMPENVMAT10 =
9,
-
D3DTSS_BUMPENVMAT11 =
10,
-
D3DTSS_TEXCOORDINDEX =
11,
-
D3DTSS_BUMPENVLSCALE =
22,
-
D3DTSS_BUMPENVLOFFSET =
23,
-
D3DTSS_TEXTURETRANSFORMFLAGS =
24,
-
D3DTSS_COLORARG0 =
26,
-
D3DTSS_ALPHAARG0 =
27,
-
D3DTSS_RESULTARG =
28,
-
D3DTSS_CONSTANT =
32,
-
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(
0, D3DTSS_COLOROP, D3DTOP_SELECTARG1);
//将纹理颜色混合的第一个参数的颜色值用于输出
-
m_pd3dDevice->SetTextureStageState(
0, D3DTSS_COLORARG1, D3DTA_TEXTURE );
//纹理颜色混合的第一个参数的值就取纹理颜色值
第一句SetTextureStageState中我们表示要将纹理颜色混合的第一个参数的颜色值用于输出,然后第二句马上就把第一个参数的颜色值取为纹理颜色值了,这样我们颜色混合后的值就是纹理的颜色值。
解决了纹理颜色混合的问题,后面就好解决了,设置世界矩阵,关联顶点和渲染流水线,设置顶点格式,接着一个for循环设置纹理并渲染,最后再判断一下是否要绘制出线框,一气呵成。实现代码就是这样:
-
//--------------------------------------------------------------------------------------
-
// Name: SkyBoxClass::RenderSkyBox()
-
// Desc: 绘制出天空盒,可以通过第二个参数选择是否绘制出线框
-
//--------------------------------------------------------------------------------------
-
void SkyBoxClass::RenderSkyBox( D3DXMATRIX
pMatWorld, BOOL bRenderFrame )
-
{
-
m_pd3dDevice->SetTextureStageState(
0, D3DTSS_COLOROP, D3DTOP_SELECTARG1);
//将纹理颜色混合的第一个参数的颜色值用于输出
-
m_pd3dDevice->SetTextureStageState(
0, D3DTSS_COLORARG1, D3DTA_TEXTURE );
//纹理颜色混合的第一个参数的值就取纹理颜色值
-
m_pd3dDevice->SetTransform( D3DTS_WORLD, pMatWorld );
//设置世界矩阵
-
m_pd3dDevice->SetStreamSource(
0,m_pVertexBuffer,
0,
sizeof(SKYBOXVERTEX));
//把包含的几何体信息的顶点缓存和渲染流水线相关联
-
m_pd3dDevice->SetFVF(D3DFVF_SKYBOX);
//设置FVF灵活顶点格式
-
-
//一个for循环,将5个面绘制出来
-
for(
int i =
0; i<
5; i++)
-
{
-
m_pd3dDevice->SetTexture(
0, m_pTexture[i]);
-
m_pd3dDevice->DrawPrimitive(D3DPT_TRIANGLESTRIP, i
4,
2);
-
}
-
-
//对是否渲染线框的处理代码
-
if (bRenderFrame)
//如果要渲染出线框的话
-
{
-
m_pd3dDevice->SetRenderState(D3DRS_FILLMODE, D3DFILL_WIREFRAME);
//把填充模式设为线框填充
-
//一个for循环,将5个面的线框绘制出来
-
for(
int i =
0; i<
5; i++)
-
{
-
m_pd3dDevice->DrawPrimitive(D3DPT_TRIANGLESTRIP, i
4,
2);
//绘制顶点
-
}
-
-
m_pd3dDevice->SetRenderState(D3DRS_FILLMODE, D3DFILL_SOLID);
//把填充模式调回实体填充
-
}
-
}
Ⅴ.析构函数
最后再实现一下析构函数,看有什么COM接口对象,SAFE_RELEASE就行了:
-
//-------------------------------------------------------------------------------------------------
-
// Desc: 析构函数
-
//-------------------------------------------------------------------------------------------------
-
SkyBoxClass::~SkyBoxClass(
void)
-
{
-
SAFE_RELEASE( m_pVertexBuffer );
-
for(
int i=
0; i<
5; i++)
-
{
-
SAFE_RELEASE( m_pTexture[i] );
-
}
-
}
这样,一个封装了天空盒的SkyBoxClass类就被我们实现出来了,可以看到,非常简单,只需要填写好六个面的24个顶点,最后为每个面贴上纹理就可以了。
四,天空盒类的使用
别看这个SkyBoxClass天空盒类写起来还有些小麻烦,但是用起来非常方便。
Ⅰ.首先,定义一个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(
20000);
//设置天空盒的边长
这里的GameMedia\topsnow1.jpg表示在工程文件夹下的GameMedia文件夹中的topsnow1.jpg图片。
Ⅲ.最后,就是在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=matTransSkymatRotSky;
-
g_pSkyBox->RenderSkyBox(&matSky,
false);