XNA Shader编程教程15-动态环境映射

程序截图

上个教程我们制作了一个透射post process shader,比起教程13的实现了一个更加真实的透明效果,本教程学习使用立方贴图实现动态环境映射。

动态环境映射

什么是动态环境贴图?一个动态环境贴图是一张表示周围环境的纹理,这个纹理每帧动态生成。这个纹理是一种特殊的纹理,叫做立方体纹理,包含六张2D纹理:如图15-1:

图15.1

如图15.1所示,立方贴图是一个展开的立方体。每个面表示环境的一张图片,你需要将相机设置到正确的方向,这个例子中只是绘制了反射在物体上的立方纹理而不绘制立方贴图本身。

当我们获取了立方贴图后,我们就将它传递到shader中,shader会将一个反射向量作为查询表使用这个环境贴图。反射向量可以这样创建(在前面的教程中我们已经看到过很多次了):

R = 2 * N·L * N - L

图15.2

图15.2 R是反射向量,L是指向光源的方向,N是法线。

有了反射向量,我们使用它作为立方贴图的查询纹理。向量中的最后那个分量决定使用那个面,另外两个对应被选择那个面的UV坐标。在现实世界中,只有100%反光的物体才会反射所有光线。通常,光线会在物体中散射和折射,最终穿过物体或被物体吸收。这次我们只实现反射,下个教程实现折射。

实现shader

这个shader需要获取立方纹理,计算反射向量。首先定义一个立方纹理:

texture ReflectionCubeMap; 
samplerCUBE ReflectionCubeMapSampler = sampler_state
{
    texture = <ReflectionCubeMap>; 
}; 
			

我们创建了一个普通的纹理和samplerCUBE,samplerCUBE可以使用一个3D向量作为纹理坐标代替我们以前用的2D向量。

然后计算反射向量,并将它作为samplerCUBE 中的查询向量。下面是代码:

float Diff = saturate(dot(L, N)); 

// Calculate reflection vector 
float3 Reflect = normalize(2 * Diff * N - L);  

然后使用Reflect在立方贴图中查询像素颜色:

float3 ReflectColor = texCUBE(ReflectionCubeMapSampler, Reflect); 

好了,你如要100%反射,只需在pixel shader中返回ReflectColor就可以了。但我们还想加上环境光,漫反射和镜面反射,只需用ReflectColor分别乘以ambient,diffuse和specular:

return Color*vAmbient*float4(ReflectColor,1) + Color*vDiffuseColor * 
Diff*float4(ReflectColor,1) + vSpecularColor * Specular*float4(ReflectColor,1);	

使用shader

要使用shader,我们需要生成立方贴图纹理,将场景绘制到这个纹理中并将它传递到shader。幸运的是,XNA支持立方贴图,而且用起来很简单。

首先声明一个立方纹理和一个渲染目标。这个渲染目标用来绘制场景包含六个渲染目标,每个目标对应立方体的一个面。然后把这个渲染目标复制到纹理,再将这个纹理传递到shader中。首先声明两个变量:

RenderTargetCube RefCubeMap; 
TextureCube EnvironmentMap;  

然后初始化:

RefCubeMap = new RenderTargetCube(this.GraphicsDevice, 256, 1, SurfaceFormat.Color);

RenderTargetCube函数需要graphics device对象,每个纹理的大小(这里是256x256 ),mipmap的级别和SurfaceFormat。

因为场景每帧绘制六次,所以我们在不损失质量的前提下尽量缩小贴图的大小。你只需设置一个面的纹理大小,立方纹理的必须是正方形(64x64,128x128,256x256……)。接下来我们将这个纹理传递到shader:

effect.Parameters["ReflectionCubeMap"].SetValue(EnvironmentMap); 

最后将场景绘制到立方渲染目标的不同面上,并将这些渲染目标复制到EnvironmentMap纹理中。

for (int i = 0; i < 6; i++)
{
    // render the scene to all cubemap faces 
    CubeMapFace cubeMapFace = (CubeMapFace)i; 
    switch (cubeMapFace)
    {
        case CubeMapFace.NegativeX:
        {
            viewMatrix = Matrix.CreateLookAt(Vector3.Zero, Vector3.Left, Vector3.Up); 
            break; 
        }
        case CubeMapFace.NegativeY: 
        {
            viewMatrix = Matrix.CreateLookAt(Vector3.Zero, Vector3.Down, Vector3.Forward); 
            break; 
        }
        case CubeMapFace.NegativeZ: 
        {
            viewMatrix = Matrix.CreateLookAt(Vector3.Zero, Vector3.Backward, Vector3.Up); 
            break;
        }
        case CubeMapFace.PositiveX: 
        {
            viewMatrix = Matrix.CreateLookAt(Vector3.Zero, Vector3.Right, Vector3.Up); 
            break; 
        }
        case CubeMapFace.PositiveY: 
        {
            viewMatrix = Matrix.CreateLookAt(Vector3.Zero, Vector3.Up, Vector3.Backward); 
            break; 
        }
        case CubeMapFace.PositiveZ: 
        {
            viewMatrix = Matrix.CreateLookAt(Vector3.Zero, Vector3.Forward, Vector3.Up); 
            break; 
        }
    }
    effect.Parameters["matWorldViewProj"].SetValue(worldMatrix * viewMatrix * projMatrix); 
    // Set the cubemap render target, using the selected face 
    this.GraphicsDevice.SetRenderTarget(0, RefCubeMap, cubeMapFace); 
    this.GraphicsDevice.Clear(Color.White); this.DrawScene(false); 
}
graphics.GraphicsDevice.SetRenderTarget(0, null); 
this.EnvironmentMap = RefCubeMap.GetTexture(); 
			

上面的代码中我们首先进行一个循环,遍历立方贴图的每个面(见图15.1,看一下面对应的数字)。面存储在一个CubeMapFace变量中,这个变量包含数字[0,5]。

现在知道了正在使用哪个面,我们必须正确设置相机。只需使用Matrix.CreateLookAt(Position, Target, Up)方法。我们知道物体在0.0,这样使用Vector.Up, Vector.Down等设置目标就能让相机指向正确的方向。

下面就可以绘制场景了。注意在自定义的DrawScene方法中有一个bool变量,表示我们是绘制透射还是环境。当绘制环境时,我们并不想施加透射效果。我只是简单地加了一个if语句,如果是true则绘制透射,如果false则绘制环境。

绘制完六个面后就可以将渲染目标复制到纹理中去了。


发布时间:2009/5/11 下午2:15:11  阅读次数:9966

2006 - 2024,推荐分辨率 1024*768 以上,推荐浏览器 Chrome、Edge 等现代浏览器,截止 2021 年 12 月 5 日的访问次数:1872 万 9823 站长邮箱

沪 ICP 备 18037240 号-1

沪公网安备 31011002002865 号