[翻译]XNA 3.0 Game Programming Recipes之twenty

PS:自己翻译的,转载请著明出处格
                                             3-13.创建一个镜像:投影纹理
问题
    您想要建立镜像在你的场景,例如中,建立一个倒车镜在赛车游戏中。您也可以使用这种技术建立一个倒影的图。
解决方案
    首先,您绘制场景,因为它被一面镜子看到,成为一个纹理。接下来,您可以绘制现场被相机看到(包括空镜子) ,最后你把纹理放到镜子里。
    为绘制被镜子看见的场景,你会要确定第二个摄像头,称为镜子相机。您可以找到此镜相机的位置,目标和Up向量反映出正常的摄象机的位置,目标和Up向量在镜子平面上。当你通过镜子摄象机观察这个场景时,你会看到镜子显示什么和实际一样。图3-24显示了这一原理。
    在您储存这个结果在一个纹理后,你将绘制这个场景用正常的摄象机,把镜子上的纹理使用投影纹理,这可以确保正确的像素映射到正确的位置在镜子上。
   如果在镜子相机和镜子之间有一个对象的话,这种方法将失败。 您可以解决这个问题使用确定镜子平面作为剪辑平面,所以在镜子背后的对象都是被截断了。
它是如何工作的
    开始添加这些变量到你的工程中:
1 RenderTarget2D renderTarget;
2 Texture2D mirrorTexture;
3 VertexPositionTexture[] mirrorVertices;
4 Matrix mirrorViewMatrix;
     renderTarget和mirrorTexture变量是需要的,因为您需要绘制现场可以被看见由镜子把它转化成一个自定义绘制的目标,3-8有详细的解释 。要创建镜子摄像头,您需要定义一个镜子视景矩阵。既然你要实际把你的镜子放到场景中,您需要定义一些顶点来定义镜子的位置。
    renderTarget变量需要被初始化在LoadContent方法中。更多的信息在配置和使用自定义绘制目标,参看3-8节。
1 PresentationParameters pp=device.PresentationParameters;
2 int width=pp.BackBufferWidth;
3 int height=pp.BackBufferHeight;
4 renderTarget=new RenderTarget2D(device,width,height,1,device.DisplayMode.Forma
t);
提示 如果您想,您就可以减少渲染目标的宽度和高度。通过这种方式,它将使您的图形卡花费很少的力气,但由此而来的镜子图像的外观粗糙。
    随着自定制renderTarge初始化,下一步就是确定你的镜子的位置:
 1 private void InitMirror()
 2 {
 3    mirrorVertices=new VertexPositionTexture[4];
 4    int i=0;
 5    Vector3 p0=new Vector3(-3,0,0);
 6    Vector3 p0=new Vector3(-3,6,0);
 7    Vector3 p0=new Vector3(6,0,0);
 8    Vector3 p3=p1+p2-p0;
 9    mirrorVertices[i++]=new VertexPositionTexture(p0,new Vector2(0,0));
10    mirrorVertices[i++]=new VertexPositionTexture(p1,new Vector2(0,0));
11    mirrorVertices[i++]=new VertexPositionTexture(p2,new Vector2(0,0));
12    mirrorVertices[i++]=new VertexPositionTexture(p3,new Vector2(0,0));
13    mirrorPlane=new Plane(p0,p1,p2);
14 }
   虽然您可以使用此方法创建一个任意形状的镜子,在这个例子中您可以创建一个简单的矩形镜子。您可以使用镜面来显示自定义渲染目标的内容。你会得出两个三角形确定矩形使用TriangleStrip,所以4个顶点将是足够的。
   在三维空间,您只需要3点来独特的定义一个长方形。这种方法让您可以指定镜子的三个角点p0 ,p1,和P2,代码将计算最后p3的点。这可以确保四点是在一个平面。该方法中最有趣的一行简单如下:
1 Vector3 p3=p0+(p1-p0)+(p2-p0);
   这四个顶点是由这4个位置创建的。这种技术并不需要任何纹理坐标传递给顶点着色器中(见晚些时候顶点着色器),但因为我懒得确定VertexPosition结构在这节,我只是传递一些任意纹理坐标,例如(0,0),因为它们不会被使用。(参看5-13如何建立自定义的顶点格式)。
注意:由于所有的Z坐标在这个例子中是0 ,这个镜子将在XY平面。
构建镜子相机的视图矩阵
    下一步,你将要创建一个镜像视景矩阵,您可以使用绘制由镜子看见的场景。要创建此镜视景矩阵,您将需要镜子相机的位置,目标和Up向量 。镜子的视景矩阵的位置,目标和Up向量是和正常摄象机同样的,但是反映在镜子平面里,如图图3-24。
1 private void UpdateMirroViewMatrix()
2 {
3    Vector3 mirrorCamPosition=MirrorVector3(mirrorPlane,fpsCam.Position);
4    Vector3 mirrorTargetPosition=MirrorVector3(mirrorPlane,fpsCam.TargetPosition);  
5    Vector3 camUpPosition=fpsCam.Position+fpsCam.UpVector;
6    Vector3 mirrorCamUpPosition=MirrorVector3(mirrorPlane,camUpPosition);
7    Vector3 mirrorUpVector=mirrorCamUpPosition-mirrorCamPosition;
8    mirrorViewMatrix=Matrix.CreateLookAt(mirrorCamPosition,mirrorTargetPosition,mirrorUpVector); 
9 }
    PositionTargetPosition的值能简单的被镜像,因为他们在3D空间的绝对位置。Up向量,尽管,声明一个方向,不会被立即的镜像。首先你需要去改变这个Up方向转换成3D位置在您的相机以上的某处。你可以实现靠添加Up方向到你相机的3D位置。
   由于这是一个三维位置,可以镜像它到你的镜子平面。你所获得的某处的三维位置上方的反射镜的摄像头,所以你减去你镜子相机的位置去获得你镜子相机的Up方向!
   一旦你知道镜子相机的位置,目标和Up方向,你可以为你的镜子相机建立一个视景矩阵。
   MirrorVector3方法将映出Vector3传递它到平面上。目前, 因为在这一节的此镜像建立在XY平面上,所有你需要做找到镜像的位置是改变Z成分的轨迹:
1 private Vector3 MirrorVector3(Plane mirrorPlane,Vector3 originalV3)
2 {
3     Vector3 mirroredV3=originalV3;
4     mirroredV3.Z=-mirroredV3.Z;
5     return mirroredV3;
6 }
   你可以找到真正的方法,使反映了一个任意平面在某个瞬间,但在这一数学方法可以绘制你关心的远离的大图片。
1 UpdateMirrorViewMatrix();
Rendering the Scene As Seen by the Mirror
     现在您已确定您的镜子视景矩阵将是正确的在任何时候,您准备好绘制你的场景,看到了一面镜子,利用这个镜子的视景矩阵。因为您将需要绘制你的场景两次(一次所看到的一面镜子,然后又被正常摄像头所看见) ,这是一个好主意,以重构您的代码,实际绘制你的场景用一个单独的方法:
 1 private void RenderScene(Matrix viewMatrix,Matrix projectionMatrix)
 2 {
 3      Matrix worldMatrix=Matrix.CreateScale(0.01f,0.01f,0.01f)*Matrix.CreateTranslation(0,0,5);
 4      myModel.CopyAbsoluteBoneTransformsTo(modelTransforms);
 5      foreach(ModeMesh mesh in myModel.Meshes)
 6      {
 7         foreach(BasicEffect effect in mesh.Effects)
 8         {
 9             effect.EnableDefaultLighting();
10             effect.World=modelTransforms[mesh.ParentBone.Index]*worldMatrix;
11             effect.View=viewMatrix;
12             effect.Projection=projectionMatrix;
13         }
14         mesh.Draw();
15      }//draw other objects of your scene.
16 }
     这个方法绘制你整个屏幕,使用View和Projection矩阵你指定的作为参数。
    在您的Draw方法,添加此代码,这样可以激活您的自定义渲染目标,并使整个现场被镜子看到,使用镜子视景矩阵转化成的自定义目标。在此之后已经做了,你停用您的自定义绘制目标靠设定后备缓冲区作为积极渲染目标(见3-9节) ,您存储自定义绘制目标的内容转化成纹理:  
1 //render scene as seen by mirror into render target
2 device.SetRenderTarget(0,renderTarget);
3 graphics.GraphicsDevice.Clear(Color.CornflowerBlue);
4 RenderScene(mirrorViewMatrix,fps.Cam.ProjectionMatrix);
5 //deactive custom render target,and save its contents into a texture
6 device.SetRenderTarget(0,nul);
7 mirrorTexture=renderTarget.GetTexture();
注意:如果是一面镜子,你想使用相同的投影矩阵,使自定义目标作为您经常使用来绘制正常现场。例如,如果您的正常投影矩阵的角度比绘制目标的矩阵更加广泛的话,你将会计算这个坐标在你的shaders里都将被混合。
    现在,这已被保存,您清除画面的返回缓冲,绘制你的场景正如正常摄像头所看到的,换句话说,使用普通视景矩阵
1 //render scene+mirror as seen by user to screen
2 graphics.GraphicsDevice.Clear(Color.Tomato);
3 RenderScene(fpsCam.ViewMatrix,fpsCam.ProjectionMatrix);
4 RenderMirror();
   最后一行调用该方法将添加镜子到你的场景中。在这个例子中, 镜子只是一个长方形界定的两个三角形,其中的颜色将抽取纹理包含场景正如一面镜子所看到的。由于镜子应该只显示正确的部分纹理,你不能简单地把图像放在矩形上,而是你必须创造一种HLSL技术为这一点。
HLSL
    一如往常,您应该首先确定变量通过从您的XNA程序传到您的着色器,纹理采样器,输出顶点的结构和像素着色器:
 1 //-------XNA interface---------
 2 float4*4 wWorld;
 3 float4*4 xView;
 4 float4*4 xProjection;
 5 float4*4 xMirrorView;
 6 //-------Texture Sampers-------
 7 Texture xMirrorTexture;
 8 sampler textureSampler=sampler_state{texture=<xMirrorTexture>;magfilter=LINEAR;minfilter=LINEAR;mipfilter=LINEAR;AddressU=CLAMP;AddressV=CLAMP;};
 9 struct MirVertexToPixel
10 {
11     float4 Position:POSITION;
12     float4 TexCoord:TEXCOORD0;
13 };
14 struct MirPixelToFrame
15 {
16     float4 Color:COLOR0;
17 };
     一如往常,您需要世界,视景和投影矩阵,您可以计算二维屏幕位置的每个三维顶点。此外,您同样还需要您的镜子摄像头的视景矩阵,它将用于在您的顶点着色器去计算正确的相应纹理坐标为每个镜子的顶点。
    您的技术效果将需要纹理包含场景正如镜子相机所看到的。 它将得到镜子的每个象素的颜色靠从正确的纹理坐标上的文理上取样。
    顶点着色器的输出将会是这一纹理坐标,以及通常的当前顶点的二维屏幕坐标。像往常一样,您的支持Pixel Shader将只计算每个像素的颜色。
顶点着色器
    其次是顶点着色器。与往常一样,顶点着色器已计算二维屏幕坐标为每个顶点。您这样做靠顶点的三维位置乘以WorldViewProjection矩阵来实现。
1 //------------Technique:Mirror------------
2 MirVertexToPixel MirrorVS(float4 inPos:POSITION)
3 {
4     MirVertexToPixel Output=(MirVertexToPixel)0;
5     float4*4 preViewProjection=mul(xView,xProjection);
6     float4*4 preWorldViewProjection=mul(xWorld,preViewProjection);
7     Output.Position=mul(inPos,preWorldViewProjection);
8 }
      镜象技术,为镜子的每个顶点,顶点着色器同样必须去计算在xMirrorTexture中顶点对应该哪个像素。可视化这一点,说你 要查找该像素在xMirrorTexture左上角镜子的顶点相应的象素。关键在寻找答案是从镜子相机看镜子。 您需要找到在二维坐标镜子相机保存的顶点在xMirrorTexture中。这正是你得到的,如果您转变顶点的3D坐标用镜子相机的WorldViewProjection矩阵。
1 float4*4 preMirrorViewProjection=mul(xMirrorView,xProjec
tion);
2 float4*4 preMirrorWorldViewProjection=mul(xWorld,preMirrorViewProjection);
3 Output.TexCoord=mul(inPos,preMirrorWordViewProjection);
4 return Output;
注意 这个词Mirror在此代码,但并不意味着额外的矩阵,它只是指矩阵属于镜子相机而不是正常的相机。作为一个例子,xMirrorView不是视景矩阵乘以一个镜子矩阵,它只镜子相机的视景矩阵。
Pixel Shader
    现在在您的像素着色器,为一面镜子的每个四个顶点的,你有相应的纹理坐标可用。剩下的唯一问题是,他们是不是在你想要的范围内。如你所知,纹理坐标介于0和1 ,如图3-25左边所示。屏幕坐标,虽然,范围是从-1到1,如图3-25右边所示。

    幸运的是,可以很容易地映射从[-1,1]范围到[0,1]的范围。例如,您可以除以2,这样范围成为[-0.5,0.5],然后加0.5,范围又变成了[0,1].
    此外,由于你处理float4(均匀)坐标,然后才能使用头三个组成部分,您需要分割他们用第四次坐标。这是什么正在被实现的Pixel Shader的第一部分 :
1 MirPixelToFrame MirrorPS(MirVertexToPixel PSIn):COLOR0
2 {
3      MirPixelToFrame Output=(MirPixelToFrame)0;
4      float2 ProjectedTexCoords;
5      ProjectedTexCoords[0]=PSIn.TexCoord.x/PSIn.TexCoord.w/2.0f+0.5f;
6      ProjectedTexCoords[1]=-PSIn.TexCoord.y/PSIn.TexCoord.w/2.0f+0.5f;
7      Output.Color=tex2D(textureSampler,ProjectedTexCoords);
8      return Output;
9 }
    -符号在这行是计算第二次纹理坐标是有必要的,因为场景需要绘制颠倒的到帧缓冲,所以你需要以补偿那个。最后一行查找相应的颜色在xMirrorTexure里,这颜色是被像素着色器返回的。
注意:前两部分表明二维屏幕的位置,你需要一个第四部分去分头三个组成部分,但什么是第三次坐标?它实际上是二维深度。换句话说,它是图形卡的Z轴缓冲的一个值。这是一个值介于0和1 ,其中0表示顶点近裁剪平面和1个顶点在远裁剪平面。之前,Pixel Shader是所谓计算像素的颜色,显卡首先确定是否这像素应该被绘制, 这基于目前z缓冲区为像素的深度值。欲了解更多信息的Z轴缓冲,请阅读最后一部分的2-1的最后一部分 。
    剩下的是技术的定义:
1 technique Mirror
2 {
3    pass Pass0
4    {
5        VertexShader=compile vs_1_1 MirrorVS();
6        PixelShader=compile ps_2_0 MirrorPS();
7    }
8 }
Using the Technique in XNA
      你仍然需要DrawMirror方法在您的XNA项目中,该项目将实际绘制矩形使用新创建的技术:
 1 private void RenderMirror()
 2 {
 3      mirrorEffect.Parameters["xWorld"].SetValue(Matrix.Identity);
 4      mirrorEffect.Parameters["xView"].SetValue(fpsCam.ViewMatrix);
 5      mirrorEffect.Parameters["xProjection"].SetValue(fpsCam.ProjectionMatrix);
 6      mirrorEffect.Parameters["xMirrorView"].SetValue(mirrorViewMatrix);
 7      mirrorEffect.Parameters["xMirrorTexture"].SetValue(mirrorTexture);
 8      mirrorEffect.Begin();
 9      foreach(EffectPass pass in mirrorEffect.CurrentTechnique.Passes)
10      {
11          Pass.Begin();
12          device.VertexDeclaration=new VertexDeclaration(device,VertexPositionTexture.VertexElements);
13          device.DrawUserPrimitives<VertexPositionTexture>(PrimitiveType.TriangleStrip,mirrorVertices,0,2);
14          pass.End();
15      }
16      mirrorEffect.End();
17 }
   一般世界,视景和投影矩阵设置以及xMirrorView矩阵和xMirrorTexture包含场景正如一面镜子所看见的。矩形的两个三角形被绘制作为TriangleStrip。您需要导入.fx文件到您的XNA项目,并已联系到mirrorEffect变量。
Arbitrary Mirror Plane
     在前面的示例中,特别是选择镜子平面,所以很容易镜像点。 在现实情况下,尽管,您希望能够确定任意镜子的平面。改善MirrorVector3方法,以便它能够在任何时候镜像三维空间任意的点,平面将允许您这样做:
1 private Vector3 MirrorVector3(Plane mirrorPlane,Vector3 originalV3)
2 {
3    float distV3ToPlane=mirrorPlane,DotCoordinate(originalV3);
4    Vector3 mirroredV3=originalV3-2*distV3ToPlane*mirrorPlane.Normal;
5    return mirroredV3;
6 }
          首先你想知道尽可能短的点和平面之间的距离,这个可以被镜子所在的平面的DotCoordinate方法计算出来(这距离是点垂直于平面)。如果您用这个距离乘以正常的向量,减去结果向量从这一点,你就完全结束了平面。你不想结束这个平面;你想要移动您的点有两倍远!因此, 您两倍这个向量和减去它从原始的点坐标。
        此代码可以让你基于任何三点使用一个镜子。
Defining a Mirror Clipping Plane
       这里仍然存在一个大问题:如果镜子背后有个对象,这些物体会被镜子相机所看到,因此储存在mirrorTexture里,最后,镜子像素着色器从这个纹理里取颜色,这些对象会显示在镜子,而在现实中这些物体在镜子的背后,因此应在任何情况下,都不会出现在镜子里。
      解决这个问题的办法是确定一个用户剪辑平面。这是通过定义一个平面,并让XNA知道,所有物体的在平面的一侧,不能被绘制。当然,这个平面应该是在你的镜子,使所有镜子后面的物体子不能被绘制。
    然而,这四个剪辑平面的系数必被定义在定剪辑空间(所以您的显卡有一个简单的时间决定哪些对象被绘制,哪些要剪掉) 。绘制他们从三维空间到剪辑平面,你必须转换它们用反转ViewProjection矩阵,就像这样:
1 private void UpdateClipPlane()
2 {
3     Matrix camMatrix=mirrorViewMatrix*fpsCam.ProjectionMatrix;
4     Matrix invCamMatrix=Matrix.Invert(camMatrix);
5     invCamMatrix=Matrix.Transpos(invCamMatrix);
6     Vector4 mirrorPlaneCoeffs=new Vector4(mirrorPlane.Normal,mirrorPlane.D);
7     Vector4 clipPlaneCoeffs=Vector4.Transform(-mirrorPlaneCoeffs,invCamMatrix);
8     clipPlane=new Plane(clipPlaneCoeffs);
9 }
        首先,您计算反转矩阵。接下来,您重新找回您的镜子平面的四个系数,定义在三维中。你映射他们到剪辑空间用反转矩阵来改造他们,并使用结果系数来创造裁剪平面。
        请注意-符号表明平面的一面会被裁减掉。这一切都被实现随着正常的平面的方向,这是确定的顺序,确定了点p0 ,的P1 , P2的,和P3你经常用来定义这个平面。
       直到这个clipPlane变量依靠在viewMatrix,它应该每次都被更新相机改变的位置,所以可以在Update的方法中调用它。
1 UpdateClipPlane();
    所有你需要做的下一步是传递剪辑平面到你的图形卡和激活它在你绘制你的屏幕正如你镜子相机所看到的那样。记住让它失去能力在绘制场景之前正如被正常的相机所看到的,因为这个对象是镜子后面,在正常相机的视阈内,因此是应该可以显示的:
1 //render scene as seen by mirror into render target
2 device.SetRenderTarget(0,renderTarget);
3 device.Clear(ClearOptions.Target|ClearOptions.DepthBuffer,Color.CornflowerBlue,1,0);
4 device.ClipPlanes[0].Plane=clipPlane;
5 device.ClipPlanes[0].IsEnabled=true;
6 RenderScene(mirrorViewMatrix,fpsCam.ProjectionMatrix);
7 device.ClipPlanes[0].IsEnabled=false;
注意:当您仔细看,你看到的图象在镜子里似乎是原始场景的稍微的模糊版本。这是因为计算纹理坐标将几乎从未对应一个确切的像素数量,从而使您的图形卡将平均在互相靠近的像素之间。这相当于平均对应一个模糊的操作(见2-13节) 。
代码
    参看上面的代码。

posted on 2009-07-30 11:53  一盘散沙  阅读(282)  评论(0编辑  收藏  举报

导航