深入Managed DirectX9(十八)

Rendering to Surfaces
  你是否玩过那种可以打开一个倒视镜的赛车游戏?或者可以在屏幕表面显示当前赛道的赛车游戏。这些效果都是通过把同一个场景(通常使用不同的摄像机)渲染为一个纹理来实现的。事实上,这虽然听起来很复杂,却相当容易实现。再从第五章的例子开始。

首先自然先声明将用来渲染的纹理,添加代码:

private Texture renderTexture = null;
private Surface renderSurface = null;
private RenderToSurface rts = null;
private const int RenderSurfaceSize = 128;

  这里声明了用于渲染的纹理,实际所要渲染的表面,以及用于绘制表面的助手对象。我们同时还声明了将要创建的纹理大小。在InitializeGraphics方法中,订阅device reste事件来创建纹理以及表面。添加代码:

device = new Device(0, DeviceType.Hardware, this, CreateFlags.SoftwareVertexProcessing, presentParams);
device.DeviceReset +=new EventHandler(OnDeviceReset);
this.OnDeviceReset();

接下来添加事件处理程序:

private void OnDeviceReset(object sender, EventArgs e)
{
    Device dev = (Device)sender;
    if(dev.DeviceCaps.VertexProcessingCaps.SupportsDirectionalLights)
    {
        uint masLights = (uint)dev.DeviceCaps.MaxActiveLights;
        if(maxLights > 0)
        {
            dev.Lights[0].Type = LightType.Directional;
            dev.Lights[0].Diffuse = Color.White;
            dev.Lights[0].Direction = new Vector3(0,-1,-1);
            dev.Lights[0].Update();
            dev.Lights[0].Enabled = true;
        }
        if(maxLights > 1)
        {
            dev.Lights[1].Type = LightType.Directional;
            dev.Lights[1].Diffuse = Color.White;
            dev.Lights[1].Direction = new Vector3(0,-1,1);
            dev.Lights[1].Update();
            dev.Lights[1].Enabled = true;
        }
    }
    rts = new RenderToSurface(dev,RenderSurfaceSize,RenderSurfaceSize,Format.X8B8G8R8,true,DepthFormat.D16);
    renderTexture = new Texture(dev,RenderSurfaceSize,RenderSurfaceSize,1,Usage.RenderTarget,Format.X8B8G8R8,Pool.Managed);
    renderSurface = renderTexture.GetSurfaceLevel(0);
}

  这里对系统作了一些检查。首先看看它是否支持方向光,如果可以,就打开这些灯光,并假设他支持足够的可用灯光。我们使用了2个方法光,分别在模型的前面和后面。

  创建了灯光之后,通过之前定义的常量创建助手对象。你可能注意到了,这个构造函数所需的参数大都能从device的presentaion parameter获得。这里使用了最常用的值,但通过presentParameter结构获得同样的值也是可以的。

  最后,创建纹理。注意把Usage设置为RenderTarger,因为我们很快就要在这张纹理上渲染。所有的渲染目标纹理都必须位于默认托管内存池中。同时,通过纹理获得实际的表面。

既然这里设置好的灯光,把选来SetupCamera方法中的代码删除。接下来,添加一个方法来绘制表面。代码如下:

private void RenderIntoSurface()
{
    // Render to this surface
    Viewport view = new Viewport();
    view.Width = RenderSurfaceSize;
    view.Height = RenderSurfaceSize;
    view.MaxZ = 1.0f;
    rts.BeginScene(renderSurface, view);
    device.Clear(ClearFlags.Target | ClearFlags.ZBuffer, Color.DarkBlue, 1.0f, 0);
    device.Transform.Projection = Matrix.PerspectiveFovLH((float)Math.PI / 4, this.Width / this.Height, 1.0f, 10000.0f);
    device.Transform.View = Matrix.LookAtLH(new Vector3(0,0, -580.0f), new Vector3(), new Vector3(0, 1,0));
    DrawMesh(angle / (float)Math.PI, angle / (float)Math.PI * 2.0f, angle / (float)Math.PI / 4.0f, 0.0f, 0.0f, 0.0f);
    DrawMesh(angle / (float)Math.PI, angle / (float)Math.PI / 2.0f, angle / (float)Math.PI * 4.0f, 150.0f, -100.0f, 175.0f);
    rts.EndScene(Filter.None);
}

  这里和一般的渲染方法很类似。调用了BeginScene方法和EndScene方法,设置摄像机变换,绘制mesh。当绘制纹理时,实际上就是把场景中我们需要的对象渲染到纹理上。在这里,你应该注意到我们使用了同一个device,只是把它移动到了模型的另一面而已,这样就可以把模型的背面渲染为纹理。另外我们还在场景中绘制了2个模型。这样可以模拟场景中有两个模型:默认的摄像机之后还有一个模型,只能通过另一个面向相反方向的摄像机才能同时看到2个模型。

  注意,BeginScene方法使用了即将要渲染的表面作为参数。因为我们是通过纹理来获得这个表面的,任何对这个表面的更新都将会映射到纹理上。EndScene方法可以把一个mipmap过滤器应用到纹理上。为了避免检测显卡的能力,暂时不使用任何过滤器。最后要注意的一点是我们改天了纹理场景的clear color。这样做可以清楚的显示出“真实”的场景和“其他的”场景。

  自然,最后还需要稍微修改一下渲染方法。首先,把纹理渲染到主窗口之前对纹理进行渲染。在OnPaint方法的最前面添加如下代码:

    RenderIntoSurface();

  最后,可以真正把纹理显示到屏幕上了有一个我们将在以后章节讨论的Sprite类可以完成这个任务,他可以方便的使用屏幕坐标绘制纹理。在EndScene方法之前,添加如下代码:

using (Sprite s = new Sprite(device))
{
    s.Begin(SpriteFlags.None);
    s.Draw(renderTexture, new Rectangle(0, 0, RenderSurfaceSize, RenderSurfaceSize),new Vector3(0, 0, 0), new Vector3(0, 0, 1.0f), Color.White);
    s.End();
}

这段代码把纹理渲染到了屏幕的左上方,现在运行程序来看看吧。



渲染环境贴图(Rendering Environment Maps)
  环境贴图是用来模拟反光很强烈的表面的一门技术。你因该在赛场游戏里见过这种效果,赛车表面反映出天上的云,或者冰面上映出冰球选手的影子。实现环境贴图最常见的方法就是使用立体纹理(cube texture)也称天空盒(一个有六面的立方体纹理),现在就来学习这门技术吧。

  在开始编写代码之前,创建一个新工程,做好各种必要准备。我们将用到SDK中的两个模型:一个赛车模型,一个天空盒模型(带有纹理)。接下来就可以写代码了,先添加变量:

private Mesh skybox = null;
private Material[] skyboxMaterials;
private Texture[] skyboxTexture;
private Mesh car = null;
private Material[] carMaterials;
private Texture[] carTexture;
private CubeTexture environment = null;
private RenderToEnvironmentMap rte = null;
private const int CubeMapSize = 128;
private readonly Matrix ViewMatrix = Matrix.Translation(0.0f,0.0f,13.0f);

  这里声明了将要绘制的两个模型:天空盒(“环境”)与我们希望环境所反射到的对象——车。同时还需要cube texture对象来保存环境,以及用于渲染环境贴图的助手类。

由于并不是所有图形卡都支持立体纹理,需要对显卡做一点点检测。在InitializeGraphics方法中完成这个任务,添加代码:

public bool InitializeGraphics()
{
    // Set our presentation parameters
    PresentParameters presentParams = new PresentParameters();
    presentParams.Windowed = true;
    presentParams.SwapEffect = SwapEffect.Discard;
    presentParams.AutoDepthStencilFormat = DepthFormat.D16;
    presentParams.EnableAutoDepthStencil = true;
    // Create our device
    device = new Device(0, DeviceType.Hardware, this, CreateFlags.SoftwareVertexProcessing, presentParams);
    device.DeviceReset += new EventHandler(OnDeviceReset);
    OnDeviceReset(device,null);
    if(!device.DeviceCaps.TextureCaps.SupportsCubeMap)
        return false;
    //load our mesh
    skybox = LoadMesh(@"..\..\skybox2.x",ref skyboxMaterials,ref skyboxTextures);
    car = LoadMesh(@"..\..\car.x",ref carMaterials,ref carTexture);
    return true;
}

  这里主要的改变就是InitializeGraphics方法现在会返回一个布尔值来显示是否执行成功了。同时他订阅了DeviceReset事件,并使用一个新的LoadMesh方法来加载模型。在编写这几个方法之前,更新main()方法来处理InitializeGraphics返回的值。

static void Main()
{
    using (Form1 frm = new Form1())
    {
        frm.Show();
        if(!frm.InitializeGraphics())
            MessageBox.Show("your card does not support cube maps.");
        else
            Application.Run(frm);
    }
}

  这里没有什么特别内容,接下来看看LoadMesh方法。和之前的方法差不多。加载了Mesh,纹理,材质之后,保证数据包含法线信息。之后,返回mesh。添加代码:

private Mesh LoadMesh(string file,ref Material[] meshMaterials,ref Texture[] meshTextures)
{
    ExtendedMaterial[] mtrl;
    Mesh mesh = Mesh.FromFile(file, MeshFlags.Managed, device, out mtrl);
    if ((mtrl != null) && (mtrl.Length > 0))
    {
        meshMaterials = new Material[mtrl.Length];
        meshTextures = new Texture[mtrl.Length];
        for (int i = 0; i < mtrl.Length; i++)
        {
            meshMaterials[i] = mtrl[i].Material3D;
            if ((mtrl[i].TextureFilename != null) && (mtrl[i].TextureFilename != string.Empty))
                meshTextures[i] = TextureLoader.FromFile(device, @"..\..\" + mtrl[i].TextureFilename);
        }
    }
    if((mesh.VertexFormat & VertexFormats.Normal) != VertexFormats.Normal)
    {
        Mesh tempMesh = mesh.Clone(mesh.Options.value,mesh.VertexFormat | VertexFormats.Normal,device);
        tempMesh.ComputeNormals();
        mesh.Dispose();
        mesh = tempMesh;
    }
    return mesh;
}

接下来,就像上一个例子,创建渲染目标表面来储存环境贴图。在OnDeviceReset方法中实现:

private void OnDeviceReset(object sender, EventArgs e)
{
    Device dev = (Device)sender;
    rte = new RenderToEnvironmentMap(dev, CubeMapSize, 1, Format.X8R8G8B8, true, DepthFormat.D16);
    environment = new CubeTexture(dev, CubeMapSize, 1,Usage.RenderTarget, Format.X8R8G8B8, Pool.Default);
}

  这里同时创建了助手对象和立体纹理。这里使用的尺寸越大(这里使用了CubeMapSize常理),环境贴图的细节就越丰富。通常情况下,把环境贴图保存为静态的会更加高兴,这样就可以直接通过一个文件(或其他数据源)加载立体纹理。这里为了展示如何在渲染时动态创建环境贴图才这么做,接下来创建所渲染的目标纹理。

  和上一个知渲染一个纹理的例子差不多,这里我们需要在场景中进行多次渲染。这里需要额外的六次渲染。(立方体的每个面一次),添加以下代码来渲染环境贴图):

private void RenderSceneIntoEnvMap
{
    Matrix matProj;
    matProj = Matrix.PerspectiveFovLH((float)Math.PI * 0.5f,1.0f,0.5f,1000.0f);
    Matrix matViewDir = ViewMatrix;
    matViewDir.M41 = 0.0f; matViewDir.M42 = 0.0f; matViewDir.M43 = 0.0f;
    if(environment != null)
        rte.BeginCube(environment);
    for(int i=0;i<6;i++)
    {
        rte.Face((CubeMapFace)i,1);
        Matrix matView = Matrix.Multiply(matViewDir,GetCubeMapViewMatrix((CubeMapFace)i));
        enderScene(matView,matProj,false);
    }
    rte.End(1);
}

  这个方法完成了很多工作。首先,我们创建了一个视野范围为90度的投影矩阵,因为立方体每个面之间的夹角恰好为90度。接下来保存这个矩阵,并且修改了最后一行,以保证对于每个面,都可以使用这个矩阵和观察矩阵相乘。

  接下来,调用助手类的BeginCube方法,让系统知道我们即将开始创建立体的环境贴图。这个类还有一些其他的方法可以用来创建不同类型的环境贴图,包括BeginHemisphere、BeginParabolic(它将使用2个面,一个在z轴正向,一个位于z轴负方向)、以及BeginSphere(使用一个单独的面)。

  在准备好了渲染环境贴图之后,对每个面作一次迭代:对每个面来调用face方法,这个方法和渲染时的BeginScene方法类似,表示上一个面已经处理完毕,可以渲染新的面了。之后,把当前矩阵与通过GetCubeMapViewMatrix获得的矩阵相乘。这个方法的定义如下:

private Matrix GetCubeMapViewMatrix(CubeMapFace face)
{
    Vector3 vEyept = new Vector3();
    Vector3 vLookDir = new Vector3();
    Vector3 vUpDir = new Vector3();
    switch(face)
    {
        case CubeMapFace.PositiveX:
            vLookDir = new Vector3(1.0f, 0.0f, 0.0f);
            vUpDir = new Vector3(0.0f, 1.0f, 0.0f);
        break;
        case CubeMapFace.NegativeX:
            vLookDir = new Vector3(-1.0f, 0.0f, 0.0f);
            vUpDir = new Vector3(0.0f, 1.0f, 0.0f);
        break;
        case CubeMapFace.PositiveY:
            vLookDir = new Vector3(0.0f, 1.0f, 0.0f);
            vUpDir = new Vector3(0.0f, 0.0f,-1.0f);
        break;
        case CubeMapFace.NegativeY:
            vLookDir = new Vector3(0.0f,-1.0f, 0.0f);
            vUpDir = new Vector3(0.0f, 0.0f, 1.0f);
        break;
        case CubeMapFace.PositiveZ:
            vLookDir = new Vector3(0.0f, 0.0f, 1.0f);
            vUpDir = new Vector3(0.0f, 1.0f, 0.0f);
        break;
        case CubeMapFace.NegativeZ:
            vLookDir = new Vector3(0.0f, 0.0f,-1.0f);
            vUpDir = new Vector3(0.0f, 1.0f, 0.0f);
        break;
    }
    Matrix matView = Matrix.LookAtLH(vEyePt,vLookDir,vUpDir);
    return matView;
}

  根据不同的面,我们修改观察参数,最后依据这些点返回观察矩阵。到这里,做完了这些之后,就可以开始渲染一个还没有车的场景了。添加RenderScene方法:

private void RenderScene(Matrix View,Matrix Project,bool shouldRenderCar)
{
    device.Transform.World = Matrix.Scaling(10.0f,10.0f,10.0f);
    Matrix matView = View;
    matView.M41 = matView.M42 = matView.M43 = 0.0f;
    device.Transform.View = matView;
    device.Transform.Projection = Profect;
    device.TextureState[0].ColorArgument1 = TextureArgument.TextureColor;
    device.TextureState[0].ColorOperation = TextureOperation.SelectArg1;
    device.SamplerState[0].MinFilter = TextureFilter.Linear;
    device.SamplerState[0].MagFilter = TextureFilter.Linear;
    device.SamplerState[0].AddressU = TextureAddress.Mirror;
    device.SamplerState[0].AddressV = TextureAddress.Mirror;
    device.RenderState.ZBufferFunction = Compare.Always;
    DrawSkyBox();
    device.RenderState.ZBufferFunction = Compare.LessEqual;
    if(shouldRenderCar)
    {
        device.Transform.View = View;
        device.Transform.Projection = Project;
        using(VertexBuffer vb = car.VertexBuffer)
        {
            using(IndexBuffer ib = car.IndexBuffer)
            {
                device.SetStreamSource(0,vb,0,VertexInformation.GetFormatSize(car.VertexFormat));
                device.VertexFormat = car.VertexFormat;
                device.Indces = ib;
                device.SetTexture(0,environment);
                device.SamplerState[0].MinFilter = TextureFilter.Linear;
                device.SamplerState[0].MagFilter = TextureFilter.Linear;
                device.SamplerState[0].AddressV = TextureAddress.Clamp;
                device.SamplerState[0].AddressU = TextureAddress.Clamp;
                device.SamplerState[0].AddressW = TextureAddress.Clamp;
                device.TextureState[0].ColorOperation = TextureOperation.SelectArg1;
                device.TextureState[0].ColorArgument1 = TextureArgument.TextureColor;
                device.TextureState[0].TextureCoordinateIndex =(int)TextureCoordinateIndex.CameraSpaceReflectionVector;
                device.TextureState[0].TextureTransform = TextureTransform.Count3;
                device.Transform.World = Matrix.RotationYawPitchRoll(angel/(float)Math.PI,angel/(float)Math.PI*2.0f, angle/(float)Math.PI/4.0f);
                angle += 0.01f;
                device.DrawIndexedPrimitives(PrimitiveType.TriangleList,0,0,car.NumberVertices,0,car.NumberFaces);
            }
        }
    }

  来看看这个方法干了些什么。首先,渲染天空盒。由于知道天空盒看起来有点小,因此先把它放大10倍。然后把TextureState以及SamplerState设置为渲染天空盒纹理所必须的值(对于每个状态的具体用法请查阅MSDN)。

  注意到没有,我们没有对device进行任何clear操作。因为我们不想渲染每个面时都调用这个方法,所以在渲染天空盒(它的深度值将是场景中最大的)之前改变RenderState,允许渲染天空盒的时候总是跳过深度缓冲。之后,就渲染天空盒,并且让深度缓冲功能恢复到正常状态。渲染天空盒的方法如下:

private void DrawSkyBox()
{
    for(int i=0;i<skyboxMaterials.Length;i++)
    {
        device.Material = skyboxMaterial[i];
        device.SetTexture(0,skyboxTextures[i]);
        skybox.DrawSubset(i);
    }
}

  渲染了天空和之后,我们查看否需要渲染赛车。如果需要,就重置观察和投影变换。这里,由于我们需要控制自动纹理坐标的生成(automatic texture coordinate generation),所以不使用mesh的DrawSubset方法,而是直接使用顶点和索要缓冲,并且通过device的DrawIndexedPrimitiv方法。首先获得这两个缓冲,确保device为渲染mesh做好准备。

  首先,把流的目标设置为mesh的顶点缓冲。根据mesh把顶点格式设置为正确类型,填充device的索引缓冲。最后,在设置其它状态和渲染之前,把立体纹理设置为第一状态。

这里最重要的是下面这两行代码:

device.TextureState[0].TextureCoordinateIndex =(int)TextureCoordinateIndex.CameraSpaceReflectionVector;
device.TextureState[0].TextureTransform = TextureTransform.Count3;

  由于在赛车的mesh里没有定义纹理坐标(实际上就算定义过也一样),第一行代码表示不使用任何预先定义的纹理坐标,而使用(已经变换为摄像机坐标的)反射向量(reflection vector)作为纹理坐标。这些信息都是通过固定功能渲染管道使用定点位置和法线来自动产生的,所以之前须要保证mesh包含法线信息。最后,由于使用3D立体映射,所以纹理变换告诉纹理使用3D纹理坐标。

纹理映射完成了之后,就可以更新世界坐标变换,同时调用绘制图元的方法了。马上就完成了,添加如下代码:

protected override void OnPaint(System.Windows.Forms.PaintEventArgs e)
{
    RenderSceneIntoEnvMap();
    device.BeginScene();
    RenderScene(ViewMatrix,Matrix.PerspectiveFovLH((float)Math.PI/4,this.Width/this.Height,1.0f,1000.0f),true);
    device.EndScene();
    device.Present();
    this.Invalidate();
}

对每次渲染来说,先渲染环境贴图,接下来才真正渲染场景:再次渲染环境贴图,以及赛车。运行程序看看吧。

~~~~~~~~~~~~~~~第十章完~~~~~~~~~~~~~~~~~~~~~~

  前两大部分的内容到这里就结束了,在第三部分的高级主题,我们将要学习关于HLSL和渲染骨骼动画的内容。 稍后把前两部分完整的PDF文档发上来^_^


 下载代码

posted on 2007-11-07 00:31  yurow  阅读(332)  评论(0编辑  收藏  举报

导航