通向天才之路 : 实时环境映射贴图技术(Real-time Evironmnet Mapping)
<Made In Coollen>
我小心翼翼地剪开了包着橡胶质外皮的线,祈祷着上帝再给它一次机会。每剪一刀,手心里面就冒出一层汗……当我剪了7段的时候,终于发现了问题,原来是四条线其中的两条线断掉了……我把它插进了电脑的USB口,手里握住那两条断的线以保持电路的联通。在经过漫长的三秒钟的时间的等待之后,我兴奋的看到游戏控制器的列表里面再次出现了它的名字:PSX-BSB Adaptor。这只从高二就一直伴随着我,随我一起南征北战的游戏手柄又复活了。这再次证明了一个事实——我果然是天才!
>>>>>> 时间快进:32:02:27 >>>>>>
如果你是个赛车游戏的爱好者,你一定玩过风靡全世界的赛车游戏《Need For Speed : UnderGround2》(《极品飞车:地下狂飚2》)。在这款游戏中,玩家扮演了一个靠赛车为生的地下飚车手,在永远是夜晚的城市中飞驰。在这款游戏中大量地运用了环境映射技术(Real-time Evironmnet Mapping),在下过雨的公路上看到两旁夜景的倒映以及玩家开的车上对周围环境的真实反射都是环境映射技术的功劳。这种最酷的最华丽的特效现在已经应用在了大量的实时虚拟显示游戏中,在将来它会成为游戏中必不可少的元素。
环境映射贴图技术最典型的应用就是车体的“流光”,这种现象在现实中非常普遍。当你开着车行驶在满是霓虹灯的街道上的时候,周围的灯光都会在你的车身上投射上一个光斑或光带(前提是你的车子洗的非常干净)。当车子在街道上飞驰的时候,就好像一个个五彩的斑点在你的车体上流动,产生非常炫目的效果。甚至在当你和别人的车子擦肩而过的时候,别人的车子也会映射在你的车体上。
要实现环境贴图映射首先需要一组环境信息。假设在场景里面有一辆你开的车(本文中将一直使用这样的例子),车的环境信息就是除掉车后,你在车的位置上向四周看到的全部画面。于是你就用照相机对六个不同方向照六张照片,这样就产生了一组环境信息。车上的每个象素都和这个环境信息中的一个点有对应关系(也就是所谓的映射)。最后渲染车的时候把车身上每个点在环境中对应的点和车体本身的效果做混合处理。
归纳起来要实现环境贴图映射需要执行下列的步骤:
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] = <matWorld>;
ViewTransform = <matView>;
ProjectionTransform = <matProject>;
// Pixel state
Texture[0] = <texCubeMap>;
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;
}
}
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
NOTE:
1.用静态环境贴图代替实时渲染
除了实时的渲染环境贴图之外,另外一个比较常见的做法是从文件中读取一个张预先生成的环境贴图。使用静态贴图实现起来比实时渲染简单,它不需要把场景绘制到Cube Map上,只需要在创建的时候从硬盘文件中载入即可。使用静态贴图可以大大降低渲染开销,是一种适合在低端环境中使用的技术。不过使用静态贴图会大大降低游戏真实性,举例来说,车体上的贴图不会随环境的改变而变化。
2.减少实时渲染的渲染开销
除了使用静态环境贴图外,还可以使用其他方式来降低系统开销。比如《地下狂飚2》是使用了限制环境贴图刷新率的办法来实现实时渲染,就是说每秒环境贴图只渲染一定的次数,而不是每帧都做渲染。而且这个频率可以让玩家来调整。另外一个方法是限制场景中渲染的物体的数量。比如车子开到路段A上,就只会对路段A两边的建筑进行绘制;或者可以只绘制一部分能够被清晰映射的物体。除此之外还可以减小环境贴图的大小,这会对渲染效果产生影响,车体上的环境映射效果会比较模糊。
3.使用可编程渲染管道
使用可编程渲染管道大大提高了显卡编程的灵活性,随着显卡性能的逐日提高,可以由显卡绘制出更真实的特效。对于环境贴图来说,上面的代码是使用顶点计算贴图坐标,如果使用了逐象素(Per-Pixel)计算,将会把效果表现的更加真实,当然这样做的开销太大,目前仅限于顶级显卡。
4.BLOG
访问Coollen的Blog可以获得更多信息。(Code::Life http://blog.coollen.com)
>>>>>> 时间快进:342:34:47 >>>>>>
火焰纹章是SLG的,手柄是修好的,玩起来是爽的。
音乐是钢琴的,音箱是25块的,听起来是不爽的。
台风是公测的,Bug是要改的,加班是没钱的。
写程序是要命的,时间是没有的,女友是跑掉的。
成绩是倒数的,技术是菜鸟的,前途是没有的。