环境映射贴图技术最典型的应用就是车体的“流光”,这种现象在现实中非常普遍。当你开着车行驶在满是霓虹灯的街道上时,周围的灯光会在车身上投射出一个光斑或光带(前提是车子洗得非常干净)。当车子在街道上飞驰的时候,就好像一个个五彩的斑点在你的车体上流动,产生非常炫目的效果。甚至当你和别的车子擦肩而过时,别的车子也会映射在你的车体上。
要实现环境贴图映射首先需要一组环境信息。假设在场景里有一辆车(本文中将一直使用这个的例子),车的环境信息,即除车以外,你在车的位置上向四周看到的全部画面。你用相机对六个不同方向拍出六张照片,这样就产生了一组环境信息。车上的每个象素都和这个环境信息中的一个点有对应关系(也就是所谓的映射)。在最后渲染车时,需要把车身上每个环境对应点与车体本身的效果做混合处理。
归纳起来要实现环境贴图映射需要执行以下步骤:
A.创建环境贴图
B.把场景中的物体渲染到环境贴图上
C.渲染车体时,把原始贴图和环境贴图经过处理后最终渲染到屏幕上
准备好你的车子了吗?你一定想马上体验一下给自己车子打上环境贴图是什么感觉吧!不过在动手之前,你还需要对相关知识有所了解,否则是很难找到正确方法的。
一、什么是环境贴图?
在上面我用照相机作过比喻,在这个比喻中,那六张照片就类似一组环境贴图,它们代表了立方体的六个面。你可以想象一下,当你的眼睛处于这个立方体时,你会看到周围所有的景物,因此我们将它们称为环境贴图。
一般来说我们所使用的环境贴图都是用立方体作参照,因此通常称为立方体贴图(Cube Map)。每个Cube Map包含六个面,在DirectX中分别用+X、-X、+Y、-Y、+Z、-Z来表示,其每个面都在水平和垂直平面上都覆盖了90度的视角。
二、我们要如何操作这组Cube Map呢?
在DirectX 9中,分别使用了六个不同的标识来表示Cube Map中的各个面。这组标识叫D3DCUBEMAP_FACES,其中的D3DCUBEMAP_FACE_POSITIVE_X就代表了Cube Map中以中心点为原点的正X轴所指向的那个面,依此类推。而Cube Map的每一个面都是一个LPDIRECT3DSURFACE9对象,当你需要对其中某个面进行某些操作时,就需要先调用GetCubeMapSurface函数来得到你所指定的那个面(Surface),然后像操作一般面一样操作即可。而本文中将要对其进行的操作就是把面作为渲染目标(Render Target),把周围环境中的景物全部绘制到这个Cube Map上。因为Cube Map在每个方向上有一个面,每次绘制其中一个面,就要对场景中所有的景物进行一次渲染。这个过程的开销是十分大的。如果你的场景中某个物体上使用了环境映射,那么原来只需绘制一次的场景,就会增加6倍的渲染消耗!如果一个游戏原来跑70帧,当绘制一个使用了环境映射的特效时,马上降到了10帧!这是一个相当可怕的数字!你也许会考虑:那么这项技术会不会因为开销太大而失去使用的价值?答案是不会的。以目前人类的智慧已经可以非常好的处理好环境映射的效率问题,所以不必担心,而且在后文中我们还会详细讲解这些方法。
三、如何使用Cube Map?
当你渲染好了贴图坐标,下一步就是如何使用的问题了。要想将一张贴图绘制到屏幕上,就需要用坐标和模型的顶点进行对应。我们都知道,2D贴图使用u、v两个坐标来表示顶点对应的贴图坐标。而在Cube Map中,仅有2个量是无法表示一个点在立方体中的位置的,所以,Cube Map的贴图坐标是由3个数的向量来表示的,你可以简单把这个贴图坐标对应的颜色理解为:从盒子中心向这个3D向量方向前进直至与盒子相交,其交点的象素颜色值。比如你要让场景中的一个球映射出周围的环境,最简单的做法是把球上每个点的法线做为贴图坐标传给图形处理芯片,这样一来就能让球具有金属质感。不过,仅仅这样简单对应的话,绘制出的图将会和现实世界的映射有所出入。
四、环境映射贴图实际如何操作?为了应用程序开发方便,微软DirectX 9提供了一个ID3DXRenderToEnvMap的接口,通过这个帮助类接口,我们可以十分快速地完成对环境贴图的操作。当然,作为一个通用的接口,它不仅含有对Cube Map的操作,还支持其他一些环境贴图,比如Sphere Map等。我们可以通过查看DirectX 9的相关文档了解更多信息。
以下代码实现的即是创建环境贴图的工作:#define CUBEMAP_SIZE 128
IDirect3DCubeTexture9* m_pCubeMap;
ID3DXRenderToEnvMap m_pRenderToEnvMap;
// 创建RenderToEnvMap对象。其中m_pd3dDevice和m_d3dsdBackBuffer分别是指向Direct3D设备和显示缓存页面的指针。CUBEMAP_SIZE这个宏指定了Cube Map的边长,它关系到环境贴图的大小,值越大占用空间越多,绘制也越慢。
if( FAILED( D3DXCreateRenderToEnvMap( m_pd3dDevice, CUBEMAP_SIZE, 1,
m_d3dsdBackBuffer.Format, TRUE, D3DFMT_D16, &m_pRenderToEnvMap ) ) )
{
return E_FAIL;
}
// 创建空的Cube Map,D3DUSAGE_RENDERTARGET说明创建的Cube Map是一个渲染目标。
if( m_d3dCaps.TextureCaps & D3DPTEXTURECAPS_CUBEMAP )
{
if( FAILED( D3DXCreateCubeTexture( m_pd3dDevice, CUBEMAP_SIZE, 1,
D3DUSAGE_RENDERTARGET, m_d3dsdBackBuffer.Format, D3DPOOL_DEFAULT, &m_pCubeMap ) ) )
{
return E_FAIL;
}
}
else
{
return E_FAIL;
}
绘制过程需要先得到一个新的投影矩阵(Projection Matrix),这个矩阵设置了摄像机的视角和场景深度范围等。另外还要计算出一个新的观察矩阵(View Matrix),这样Cube Map才会在“车”上随着视角的改变而变化。
HRESULT RenderSceneIntoEnvMap(){
HRESULT hr;
// 设置投影矩阵
D3DXMATRIXA16 matProj;
D3DXMatrixPerspectiveFovLH( &matProj, D3DX_PI * 0.5f, 1.0f, 0.5f, 1000.0f );
// 得到当前观察矩阵
D3DXMATRIXA16 matViewDir( m_matView );
matViewDir._41 = 0.0f; matViewDir._42 = 0.0f; matViewDir._43 = 0.0f;
// 把场景绘制到Cube Map上
if( m_pCubeMap )
hr = m_pRenderToEnvMap->BeginCube( m_pCubeMap );
if(FAILED(hr))
return hr;
for( UINT i = 0; i < 6; i++ )
{
// 设置Cube Map中的一个面为当前的渲染目标
m_pRenderToEnvMap->Face( (D3DCUBEMAP_FACES) i, 0 );
// 计算新的观察矩阵
D3DXMATRIXA16 matView;
matView = D3DUtil_GetCubeMapViewMatrix( (D3DCUBEMAP_FACES) i );
D3DXMatrixMultiply( &matView, &matViewDir, &matView );
// 设置投影和观察矩阵并渲染场景(省略)。注意:这里的渲染场景中的物体并不包括使用环境贴图的物体。
... ...
}
m_pRenderToEnvMap->End( 0 );
return S_OK;
}
渲染出环境贴图后,需要把这个环境贴图运用在目标物体上,即将其作用在我们的车子上。我们在渲染场景(真正绘制到屏幕的时候)中使用一个特效(Effect)来绘制。特效可以从内存中一段字串创建,也可以从一个特效文件(后缀名为fx,由DirectX9 SDK自带的Effect Edit生成)来创建。特效的使用方法可以查看SDK中的相关文档和例子,因为涉及的东西非常多,所以不在这里多说了。特效文件的代码如下,这里给出了基于固定渲染管道(Fixed Function Pipeline)的实现代码,除此之外还可以使用可编程渲染管道(Programmable Pipeline)来实现。
textureCUBE texCubeMap;
matrix matWorld;
matrix matView;
matrix matProject;
technique Cube
{
pass P0
{
// Vertex state
VertexShader = null;
WorldTransform[0] = ;
ViewTransform = ;
ProjectionTransform = ;
// Pixel state
Texture[0] = ;
MinFilter[0] = Linear;
MagFilter[0] = Linear;
AddressU[0] = Clamp;
AddressV[0] = Clamp;
AddressW[0] = Clamp;
ColorOp[0] = SelectArg1;
ColorArg1[0] = Texture;
TexCoordIndex[0] = CameraSpaceReflectionVector;
TextureTransformFlags[0] = Count3;
}
}
需要注意的几点:
1.用静态环境贴图代替实时渲染
除了实时的渲染环境贴图外,另一个比较常见的做法是从文件中读取一张预先生成的环境贴图。静态贴图实现起来比实时渲染简单,它不需要把场景绘制到Cube Map上,只需在创建时从硬盘文件中载入即可。静态贴图可以大大降低渲染开销,是一种非常适合低端环境的技术。不过静态贴图会大大降低游戏的真实性,如果采用了静态贴图,那么车体上的贴图是不会随着环境改变而变化的。
2.减少实时渲染的渲染开销
除了使用静态环境贴图外,还可以使用其他方式来降低系统开销。比如《地下狂飚2》使用了限制环境贴图刷新率的办法来实现实时渲染,就是说规定每秒环境贴图只渲染一定的次数,而不是每帧都做渲染。而且这个频率可以让玩家来调整。另外一个方法是限制场景中渲染的物体数量。比如车子开到路段A上,就只会对路段A两边的建筑进行绘制;或者可以只绘制一部分能够被清晰映射的物体。除此之外还可以减小环境贴图的大小,但是这会对渲染效果产生影响,车体上的环境映射效果会比较模糊。
3.使用可编程渲染管道
使用可编程渲染管道大大提高了显卡编程的灵活性,随着显卡性能的逐日提高,可以由显卡绘制出更真实的特效。对于环境贴图来说,上面的代码是使用顶点计算贴图坐标,如果使用了逐象素(Per-Pixel)计算,将会把效果表现的更加真实,当然这样做的开销太大,目前仅限于顶级显卡。