作者:i_dovelemon
来源:CSDN
日期:2014 / 10 / 26
主题:Cube mapping, SkyBox
引言
在3D游戏中,特别是在室外游戏场景中,往往需要模拟出天空的效果。如下图所示:
在这张图片中,读者可以发现,场景中存在这天空,而且不管游戏角色如何在场景中进行移动,都无法靠近天空。通过添加这样的效果,使得游戏世界更加的丰富多彩,更加的接近现实的世界。本节,就想大家讲述,如何在DirectX中实现这样的天空效果。
Cube Mapping
Cube Mapping是Enviroment Mapping中的一种。我们知道,传统的纹理映射方式,是用一张2D的图片,映射到2D的表面上去。这样的映射方法在我的博客中关于纹理映射一章中讲述了如何一一的进行了映射。从中我们可以发现,这是一种平面上的一对一的映射方法。而Cube
mapping是一种3D空间的映射方法。对于一个3D空间的物体,我们有时候会发现使用2D的纹理来对3D物体的每一个表面进行映射,在物体的整体感觉上会差很多。那么,有没有一种方法,能够将纹理进行整体的考量,然后映射到3D物体上去,这样物体的纹理效果看上去就十分的和谐,整体上十分的舒适。使用Cube mapping就能够做到这一点。
Cube mapping是将一张由6个面组成的纹理,构成一个在逻辑上是Cube立方体的3D纹理图。也就是如下图所示的样子:
这样的一张图,它有6个面组成,分别对应了一个立方体的6个面。一般来说,我们将以如下的顺序来构成这个纹理图:
从图中可以看出,这6个面就构成了一个立方体。在有了这个逻辑上,可以作为立方体来看待的纹理图之后,我们需要的就是如何定义这个纹理上的坐标。很显然,对于3D的纹理图,自然应该使用3D空间坐标来表示。那么,这个3D空间坐标是如何映射到这个3D空间纹理上来的了。
我们来看看如下的一个例子,为了解释的方便,我在2D空间上作图,看下图:
上图中外围的立方体就是我们的3D空间的纹理,而里面的球体,就是我们希望将3D纹理映射到的物体。进行Cube mapping的时候,我们从球体的几何中心发射一条射线到我们希望映射的那个球体的点上去,这个射线同时也会和立方体相交,那么我们就可以将这个与立方体相交的点作为球体上的点的纹理。通过这样的方法,对于任何一个球体上的点,我们都可以在立方体上找到与之相对应的纹理。那么剩下的问题是,纹理坐标从何而来?
从上面的描述中,读者会发现,这条射线应该就是球体上的坐标点的坐标值(如果球心就是模型坐标的中心点的话)V(x,y,z)。那么,如何根据这个纹理坐标来获取我们那个纹理图上的纹理了?要知道,虽然逻辑上它是一个3D纹理图,但是实际在存储的时候,还是使用2D的方法进行存储的。所以,我们需要从向量V中获取与射线相交的那一个点到底在哪一个面上?
对于任何一个向量V(x,y,z),我们选取它的任何一个绝对值最大的分量,那么这个分量就标示了这条射线与哪一个面相交。比如说,对于向量V(-3,1,0)来说,这个向量所发出的射线是与立方体的左面,即-X面相交的。对于向量V(1,4,-1)来说,这个向量所发出的射线是与立方体的上面,即+Y面相交的。这是很显然的事实,读者可以自行在图上画画,了解一下。
在获取到向量与哪一个面相交之后,剩下的问题就是如何从这个面上获取纹理。这个问题就变化成为了一个2D平面上获取纹理的问题了。从纹理映射一节中,我们知道,纹理映射的方法,是纹理的左上角坐标为(0,0),右下角坐标为(1,1),即纹理坐标的u和v坐标范围是(0,1)上。那么,我们如何获取这个坐标值了?
在上面,我们使用绝对值最大的分量来确定了与哪一个面相交,那么剩下的两个分量与纹理坐标是否有联系了?答案是肯定的,读者可以想象一下,在3D空间中,当我们正对着3D纹理的一个面的时候,是不是它的坐标就对应着剩下的两个分量了?也就是说,我们通过一种线性的变换,将剩下的两个值,转化成为[0,1]这个空间来,就能够根据这个像素值来获取上面的纹理值了。
这样的线性变换十分的简单,我们知道,一个绝对值最大的分量,那么我们就可以使用这个分量的绝对值来作为参考,因为其他值的绝对值都会比这个值来的小,即,其他值除以这个值之后的值所在的范围是[-1,1]。我们对这个结果进行平移操作使得范围变成了[0,2],再在这个基础上乘以0.5就变成了[0,1]。由于前面进行变换都是线性的,所以一一对应的这种属性并没有随着变换而发生改变,即原来的值对应的纹理,在进过变换之后,依然还是这个纹理,只是从3D空间的坐标变成了2D空间的纹理坐标,便于我们在纹理图上进行访问。
上面就是进行Cube mapping的过程。这个过程在DirectX中,已经支持了。我们可以不需要自己来实现。我们的工作只是需要创建一个这样的3D纹理图。然后将这个图附加给我们想要映射的3D物体即可。在上面的讨论中,我们已经明白了进行这种映射时的纹理坐标就是3D物体本身的模型坐标。
Sky Box
好了,在讲解了Cube mapping之后,现在就来讲解下,如何实现天空。实现天空的方法,有很多。一般来讲,我们可以创建一个天空几何体,这个几何体可以是球体,也可以是立方体。这里我们使用球体来代表天空盒子。创建好了天空盒子之后,在绘制的时候,我们需要注意一点,也就是上面我们说的,不管相机如何的移动,总是没有办法接近SkyBox的边缘。而实现这样的效果,要么将天空盒子做的无限大,很显然在计算机中没有无限大的概念,那么就只有将天空盒子随着相机一起进行移动。也就是说,使的相机总是治愈天空盒子的中心处,并且将天空盒子的z值设置成为最远的z值。这样就能够保证,天空总是处于最外围的包围状态。下面是实现这个Skybox的代码。
<span style="font-family:Microsoft YaHei;">//Load the texture HR(D3DXCreateCubeTextureFromFile(m_pDevice, L"grassenvmap1024.dds", &m_pCubeTex));</span>
调用D3DXCreateCubeTextureFromFile来读取3D纹理图。这个纹理图可以通过一些软件制作得到。这里不介绍如何制作这些图片。
<span style="font-family:Microsoft YaHei;">void CubeDemo::drawCube() { UINT pass = 0 ; m_pEffect->Begin(&pass, 0); for(UINT i = 0 ; i< pass ; i ++ ) { m_pEffect->BeginPass(i); D3DXMATRIX m ; D3DXMatrixIdentity(&m); D3DXMatrixTranslation(&m, m_Camera.pos().x, m_Camera.pos().y,m_Camera.pos().z ); m_pEffect->SetMatrix(m_gWVP, &(m * m_Camera.viewproj())); m_pEffect->SetTexture(m_gTex, m_pCubeTex); m_SphereMesh->DrawSubset(0); m_SphereMesh->DrawSubset(0); m_pEffect->EndPass(); } m_pEffect->End(); }</span>
这个函数用来将天空盒子进行平移,使得相机总是至于天空盒子的中心处。
<span style="font-family:Microsoft YaHei;">//--------------------------------------------------------------------- // declaration : Copyright (c), by XJ , 2014 . All right reserved. // brief : This shader file will define the skybox using the cube mapping // file : SkyBox.fx // author : XJ // date : 2014 / 10 / 26 // version : 1.0 //------------------------------------------------------------------------ /** * Define the variant */ uniform extern float4x4 gWVP ; // the world-view-projection matrix uniform extern texture gTex ; // the cube mapping texture sampler EnvMaps = sampler_state { Texture = <gTex> ; MinFilter = LINEAR ; MagFilter = LINEAR ; MipFilter = LINEAR ; AddressU = WRAP ; AddressV = WRAP ; }; void SkyVS(float3 posL: POSITION0, out float4 posH: POSITION0, out float3 oEnvTex: TEXCOORD0) { //set the z = w, to make the z/w =1, which means the vertex is always in the far plane posH = mul((float4(posL, 1.0f)), gWVP).xyww; //save the vertex position as the texture coordinate oEnvTex = posL ; } float4 SkyPS(float3 oEnvTex: TEXCOORD0):COLOR0 { return texCUBE(EnvMaps, oEnvTex); } technique SkyBox { pass p0 { vertexShader = compile vs_2_0 SkyVS(); pixelShader = compile ps_2_0 SkyPS(); CullMode = None ; ZFunc = Always ; StencilEnable = true ; StencilFunc = Always ; StencilPass = Replace ; StencilRef = 0 ; } }</span>
这个是天空盒子的Shader文件。可以发现,我们在VS中,将齐次化之后的坐标值的z值设置为w,这样在进行最后的齐次化操作的时候,z/w = 1。而我们知道,在DirectX中,进行3次基本变换之后的空间是一个X[-1,1] - Y[-1,1] - Z[0,1]的空间。也就是说,如果z值为1的时候,就是距离最远的地方,在大的话,就会被硬件上的流水操作裁剪掉了。所以,通过这样的方法,我们就能够保证,天空盒子用于处在最远处。
同时还需要注意的是,我们这时候使用的映射方法不是以前的tex2D映射方式了,而是texCUBE映射方法。
下面是最终的截图:
好了,今天的笔记到此结束!!!