3.13 创建一面镜子:投影纹理
问题
你想在场景中创建一面镜子。例如,在一个赛车游戏中创建一面后视镜。你也可以使用这个技术创建一个反射贴图。
解决方案
首先需要将镜子中看到的场景绘制到一张纹理中。然后,绘制相机中见到的场景(包括空镜子),最后将这张纹理贴在镜子上。
要绘制从镜中看起来的场景,你需要定义第二个相机,叫做镜像相机(mirror camera)。你可以通过镜像第一个普通相机的Position,Target和Up向量获取镜像相机的Position,Target和Up向量。当通过镜像相机观看场景时,你看到的与镜子中看到的一样。图3-24 显示了这个原理。
图3-24 镜像原理
将这个结果储存到纹理中之后,你将绘制在普通相机中看到的场景和使用投影纹理将纹理绘制到镜子上,投影纹理将正确的像素映射到镜子的对应位置。如果镜像相机和镜子之间有物体存在,这个方法不适用。
你可以通过定义一个镜像剪裁平面解决这个问题,这样镜子之后的物体都会被剪裁掉。
工作原理
首先在项目添加以下变量:
RenderTarget2D renderTarget; Texture2D mirrorTexture; VertexPositionTexture[] mirrorVertices; Matrix mirrorViewMatrix;
你需要将镜子中看到的场景绘制到一个自定义的渲染目标中,所以需要renderTarget和mirrorTexture变量。要创建镜像相机,需要定义一个镜像View矩阵。因为你需要将镜子添加到场景中,所以需要一些顶点定义镜子的位置。
变量renderTarget在LoadContent方法中进行初始化。创建和使用自定义渲染目标更多的信息可参见教程3-8 。
PresentationParameters pp = device.PresentationParameters; int width = pp.BackBufferWidth; int height = pp.BackBufferHeight; renderTarget = new RenderTarget2D(device, width, height, 1, device.DisplayMode.Format);
技巧:你也可以减少渲染目标的宽和高。通过这个方式,显卡的开销更少,但镜中的图像看起来比较粗糙。
初始化渲染目标后,定义镜子的位置:
private void InitMirror() { mirrorVertices = new VertexPositionTexture[4]; int i = 0; Vector3 p0 = new Vector3(-3, 0, 0); Vector3 p1 = new Vector3(-3, 6, 0); Vector3 p2 = new Vector3(6, 0, 0); Vector3 p3 = p1 + p2 - p0; mirrorVertices[i++] = new VertexPositionTexture(p0, new Vector2(0,0)); mirrorVertices[i++] = new VertexPositionTexture(p1, new Vector2(0,0)); mirrorVertices[i++] = new VertexPositionTexture(p2, new Vector2(0,0)); mirrorVertices[i++] = new VertexPositionTexture(p3, new Vector2(0,0)); mirrorPlane = new Plane(p0, p1, p2); }
你可以使用这个方法创建任意形状的镜子,本例中,创建的是一个简单的矩形镜子。使用镜子平面显示自定义渲染目标中的内容。你需要使用TriangleStrip绘制两个三角形定义这个矩形,所以只需四个顶点。
在3D空间中,你只需三个点就可以定义一个矩形。这个方法让你可以指定镜子的顶点p0, p1和p2,代码会计算点p3,使第四个点也在同一平面内,对应的是这行代码:
Vector3 p3 = p0 + (p1 – p0) + (p2 - p0);
从这四个位置创建四个顶点。这个方法无需将纹理坐标传递到(见后面的vertex shader),但是因为我懒得定义一个VertexPosition结构,所以只是简单地传递了一些任意的纹理坐标,例如(0,0),因为它们不会被用到(可参见教程5-14学习如何创建自定义顶点格式)。
注意:因为本例中所有Z坐标为0,所以镜子在XY平面中。
构建镜像相机的View矩阵
下一步,你想创建一个镜像View矩阵,用来绘制从镜子中看起来的场景。要创建这个镜像View矩阵,你需要镜像相机的Position, Target和Up向量。镜像View 矩阵的Position, Target和Up向量是普通相机的关于镜子平面的镜像,如图3-24所示。
private void UpdateMirrorViewMatrix() { Vector3 mirrorCamPosition = MirrorVector3(mirrorPlane, fpsCam.Position); Vector3 mirrorTargetPosition = MirrorVector3(mirrorPlane, fpsCam.TargetPosition); Vector3 camUpPosition = fpsCam.Position + fpsCam.UpVector; Vector3 mirrorCamUpPosition = MirrorVector3(mirrorPlane, camUpPosition); Vector3 mirrorUpVector = mirrorCamUpPosition - mirrorCamPosition; mirrorViewMatrix = Matrix.CreateLookAt(mirrorCamPosition, mirrorTargetPosition, mirrorUpVector); }
Position和TargetPosition可以简单地被镜像,因为它们是在绝对3D空间中的。但是,Up向量表示一个方向,无法立即被镜像。你需要首先将Up方向转化为一个3D位置,在相机上方的某处,这可以通过将Up方向添加到相机的3D位置中做到。
因为这是一个3D位置,你可以取它的镜像了。你获取的是一个位于镜像相机上方的3D位置,所以减去镜像相机的位置就可以获取镜像相机的Up方向了。
知道了镜像相机的Position, Target和Up方向,你就可以创建View矩阵了。
MirrorVector3方法会根据传入的mirrorPlane参数对Vector3进行镜像。因为本教程创建的镜子位于XY平面,要找到镜像位置只需要改变Z分量的符号:
private Vector3 MirrorVector3(Plane mirrorPlane, Vector3 originalV3) { Vector3 mirroredV3 = originalV3; mirroredV3.Z = -mirroredV3.Z; return mirroredV3; }
等会你会学习如何关于任意平面进行镜像,但是这个方法中的数学原理会分散你的注意力。现在从Update 方法中调用UpdateMirrorViewMatrix方法:
UpdateMirrorViewMatrix();
绘制从镜中看到的场景
有了镜像View矩阵,你就可以使用这个矩阵绘制从镜子中看到的场景了。因为需要绘制场景两次(一次从镜子看到,一次从普通相机中看到),好主意是重构你的代码,将场景绘制放到一个独立的方法中:
private void RenderScene(Matrix viewMatrix, Matrix projectionMatrix) { Matrix worldMatrix = Matrix.CreateScale(0.01f, 0.01f, 0.01f) * Matrix.CreateTranslation(0, 0, 5); myModel.CopyAbsoluteBoneTransformsTo(modelTransforms); foreach (ModelMesh mesh in myModel.Meshes) { foreach (BasicEffect effect in mesh.Effects) { effect.EnableDefaultLighting(); effect.World = modelTransforms[mesh.ParentBone.Index] * worldMatrix; effect.View = viewMatrix; effect.Projection = projectionMatrix; } mesh.Draw(); } //draw other objects of your scene ... }
这个方法使用View和Projection矩阵作为参数绘制场景。
在Draw方法中,添加这个代码,开启自定义渲染目标并通过镜像View矩阵将镜子中看到的场景绘制到这个渲染目标中。之后,通过将后备缓冲设置为当前渲染目标关闭这个自定义渲染目标(见教程3-9),然后将自定义渲染目标中的内容储存到一个纹理中:
//render scene as seen by mirror into render target device.SetRenderTarget(0, renderTarget); graphics.GraphicsDevice.Clear(Color.CornflowerBlue); RenderScene(mirrorViewMatrix, fpsCam.ProjectionMatrix); //deactivate custom render target, and save its contents into a texture device.SetRenderTarget(0, null); mirrorTexture = renderTarget.GetTexture();
注意:在镜像的情况中,你想使用与普通场景相同的投影矩阵绘制自定义渲染目标。如果,例如,你的普通投影矩阵的角度大于渲染目标的矩阵,在shader中计算的坐标会混合在一起。
保存了纹理后,你将清除后备缓冲,然后绘制普通相机中的场景,换句话说,使用普通 View矩阵:
//render scene + mirror as seen by user to screen graphics.GraphicsDevice.Clear(Color.Tomato); RenderScene(fpsCam.ViewMatrix, fpsCam.ProjectionMatrix); RenderMirror();
最后一行代码调用的方法将镜子添加到场景中。在本例中,镜子只是一个由两个三角形定义的简单矩形,它的颜色从包含镜子中看到的场景的纹理中采样。因为镜子要显示纹理正确部分,你不能简单地将图像放置在矩形上,而是应该创建一个HLSL technique。
HLSL
首先定义XNA-HLSL变量,纹理采样器,vertex和pixel shader的输出结构:
//XNA interface float4x4 xWorld; float4x4 xView; float4x4 xProjection; float4x4 xMirrorView; //Texture Samplers Texture xMirrorTexture; sampler textureSampler = sampler_state { texture = <xMirrorTexture>; magfilter = LINEAR; minfilter = LINEAR; mipfilter=LINEAR; AddressU = CLAMP; AddressV = CLAMP; }; struct MirVertexToPixel { float4 Position : POSITION; float4 TexCoord : TEXCOORD0; }; struct MirPixelToFrame { float4 Color : COLOR0; }
和往常一样,你需要World, View和Projection矩阵计算每个3D顶点的2D屏幕位置。而且还需要镜像相机的View矩阵用来在vertex shader中计算镜子中每个顶点的对应纹理坐标。
Technique需要包含镜中看的的场景的纹理,它会通过采用这个纹理获取镜子中的每个像素的颜色。
vertex shader的输出结构是这个纹理坐标和当前顶点的2D屏幕坐标。而pixel shader只计算像素的颜色。
Vertex Shader
和往常一样,vertex shader计算每个顶点的2D屏幕坐标,这可以通过将3D位置乘以 WorldViewProjection矩阵做到。
//Technique: Mirror MirVertexToPixel MirrorVS(float4 inPos: POSITION0) { MirVertexToPixel Output = (MirVertexToPixel)0; float4x4 preViewProjection = mul(xView, xProjection); float4x4 preWorldViewProjection = mul(xWorld, preViewProjection); Output.Position = mul(inPos, preWorldViewProjection); }
在镜像technique中,对镜子的每个顶点,vertex shader还要计算顶点对应xMirrorTexture中的哪个像素。比方说你想找到xMirrorTexture中的哪个像素对应镜子的左上顶点,找到答案的关键在于从镜像相机中看镜子。你需要获取镜像相机保存在哪个xMirrorTexture 中的顶点对应哪个2D坐标,这实际上就是你通过镜像相机的WorldViewProjection矩阵转换的3D坐标。
float4x4 preMirrorViewProjection = mul (xMirrorView, xProjection); float4x4 preMirrorWorldViewProjection = mul(xWorld, preMirrorViewProjection); Output.TexCoord = mul(inPos, preMirrorWorldViewProjection); return Output;
注意:代码中的单词Mirror不表示一个附加矩阵;只是表示属于镜像相机的矩阵。例如,xMirrorView不是表示Mirror矩阵乘以View矩阵;只是表示镜像相机的View矩阵。
Pixel Shader
现在在pixel shader中,对镜子的四个顶点,你有了对应的纹理坐标。接下来的问题是哪个范围是你不想要的。而你知道纹理坐标介于0和1之间,如图3-25左图所示。而屏幕坐标的范围在–1至1之间,如图3-25右图所示。
图3-25 纹理坐标(左),屏幕坐标(右)
幸运的是,从[-1,1]映射到[0,1]很简单。例如,你可以先除以2,这样范围变为[-0.5,0.5],然后加0.5,范围变为[0,1]。而且,因为你处理的是一个float4 (齐次)坐标,在使用前三个分量前,需要将它们除以第四个坐标。这就是pixel shader的第一部分进行的操作:
MirPixelToFrame MirrorPS(MirVertexToPixel PSIn) : COLOR0 { MirPixelToFrame Output = (MirPixelToFrame)0; float2 ProjectedTexCoords; ProjectedTexCoords[0] = PSIn.TexCoord.x/PSIn.TexCoord.w/2.0f +0.5f; ProjectedTexCoords[1] = -PSIn.TexCoord.y/PSIn.TexCoord.w/2.0f +0.5f; Output.Color = tex2D(textureSampler, ProjectedTexCoords); return Output; }
计算第二个纹理坐标的代码中的“–”号是必须的,因为场景需要上下颠倒绘制到帧缓冲中,所以你需要进行调整。最后一行代码查询xMirrorTexture中的对应颜色,这个颜色返回到pixel shader中。
注意:前两个分量表示2D屏幕坐标;你需要将前三个分量除以第四个分量,但第三个坐标是什么?它实际上是2D深度。换句话说,这个值在显卡z缓冲中,介于0和1之间,表示顶点在近裁平面,1表示在远裁平面。当调用pixel shader计算像素颜色时,显卡首先根据像素在z缓冲中的深度值判断这个像素是否应该被绘制 。更多的信息可参见教程2-1的最后一部分。
下面是technique定义:
technique Mirror { pass Pass0 { VertexShader = compile vs_1_1 MirrorVS(); PixelShader = compile ps_2_0 MirrorPS(); } }
在XNA中使用Technique
你还需要在XNA项目中定义DrawMirror方法,这个方法使用刚才创建的technique绘制矩形:
private void RenderMirror() { mirrorEffect.Parameters["xWorld"].SetValue(Matrix.Identity); mirrorEffect.Parameters["xView"].SetValue(fpsCam.ViewMatrix); mirrorEffect.Parameters["xProjection"].SetValue(fpsCam.ProjectionMatrix); mirrorEffect.Parameters["xMirrorView"].SetValue(mirrorViewMatrix); mirrorEffect.Parameters["xMirrorTexture"].SetValue(mirrorTexture); mirrorEffect.Begin(); foreach (EffectPass pass in mirrorEffect.CurrentTechnique.Passes) { pass.Begin(); device.VertexDeclaration = new VertexDeclaration(device, VertexPositionTexture.VertexElements); device.DrawUserPrimitives<VertexPositionTexture> (PrimitiveType.TriangleStrip, mirrorVertices, 0, 2); pass.End(); } mirrorEffect.End(); }
设置World, View和Projection矩阵,还有xMirrorView矩阵和包含镜中看的的场景的xMirrorTexture。矩形的两个三角形以TriangleStrip的形式绘制。你需要在XNA项目导入. fx文件并将它连接到mirrorEffect变量。
任意镜像平面
在前面的例子中,选择了一个特殊位置的镜像平面,因此很容易对点进行镜像。但是在真实情况中,你还想定义任意镜像平面。这需要改进MirrorVector3方法,让它可以在任意镜像平面上对任意3D点进行镜像:
private Vector3 MirrorVector3(Plane mirrorPlane, Vector3 originalV3) { float distV3ToPlane = mirrorPlane.DotCoordinate(originalV3); Vector3 mirroredV3 = originalV3 - 2 * distV3ToPlane * mirrorPlane.Normal; return mirroredV3; }
首先你想知道点与平面间的最短距离,这可以通过镜子平面的DotCoordinate方法进行计算(这个最短距离就是点垂直于平面的距离)。如果你将平面法线乘以这个距离,从这个点减去刚才计算的结果向量,你就会位于平面上。但你不想在平面上,你想移动两倍距离!所以,你需要将这个结果向量加倍并从初始点坐标中减去这个向量。
这个代码让你可以基于任意三点使用一面镜子。
定义一个镜子剪裁平面
仍有一个大问题:当物体在镜子后面时,这些物体会被镜子相机看到并存储到mirrorTexture中。当Mirror pixel shader从这个纹理采样颜色时,这些物体会被绘制到镜子上,但实际上这些物体是在镜子之后的,不应该被显示。
解决方法是定义一个用户剪裁平面,这可以通过定义一个平面并告知XNA在镜子另一边的物体无需绘制做到。当然,这个平面应该是镜子所处平面,所以在镜子之后的物体无需绘制。
但是,剪裁平面的四个系数必须定义在剪裁空间中(这样你的显卡可以容易地判断哪些物体需要被绘制哪些需要被剪裁)。要将系数从3D空间映射到剪裁空间,你需要通过ViewProjection的反置(inverse-transpose)矩阵变换它们,如下列代码所示:
private void UpdateClipPlane() { Matrix camMatrix = mirrorViewMatrix * fpsCam.ProjectionMatrix; Matrix invCamMatrix = Matrix.Invert(camMatrix); invCamMatrix = Matrix.Transpose(invCamMatrix); Vector4 mirrorPlaneCoeffs = new Vector4(mirrorPlane.Normal, mirrorPlane.D); Vector4 clipPlaneCoeffs = Vector4.Transform(-mirrorPlaneCoeffs, invCamMatrix); clipPlane = new Plane(clipPlaneCoeffs); }
首先计算反置矩阵。然后,接受镜子平面的定义在3D空间中的四个系数,通过反置矩阵将它们映射到剪裁空间,然后使用结果创建剪裁平面。
注意:“–”表示平面的哪一个面需要被剔除。与平面的法线方向有关,是根据定义点p0, p 1, p2和p3的顺序定义的。
因为clipPlane变量取决于viewMatrix,而viewMatrix在相机位置改变时需要更新,所以在Update方法中调用下面的方法:
UpdateClipPlane();
接下来要做的就是将剪裁平面传递到显卡中,在绘制镜子中看到的场景前开启它,记住在绘制普通相机看到的场景前关闭它,因为镜子后面的物体可以被普通相机看到,应该被显示:
//render scene as seen by mirror into render target device.SetRenderTarget(0, renderTarget); device.Clear(ClearOptions.Target | ClearOptions.DepthBuffer,Color.CornflowerBlue, 1, 0); device.ClipPlanes[0].Plane = clipPlane; device.ClipPlanes[0].IsEnabled = true; RenderScene(mirrorViewMatrix, fpsCam.ProjectionMatrix); device.ClipPlanes[0].IsEnabled = false;
注意:如果你近距离观察,镜子中的图像相比原始图像看起来有些模糊。这是因为被计算的纹理坐标通常不会精确地对应一个像素,所以显卡会取最近像素的平均,平均化的过程对应模糊化的操作(见教程2-12)。
代码
要加载technique,需要将. Fx文件加载到一个Effect变量中,还需要定义镜子:
private void InitMirror() { mirrorVertices = new VertexPositionTexture[4]; int i = 0; Vector3 p0 = new Vector3(-3, 0, 1); Vector3 p1 = new Vector3(-3, 6, 0); Vector3 p2 = new Vector3(6, 0, 0); Vector3 p3 = p1 + p2 - p0; mirrorVertices[i++] = new VertexPositionTexture(p0, new Vector2(0,0)); mirrorVertices[i++] = new VertexPositionTexture(p1, new Vector2(0,0)); mirrorVertices[i++] = new VertexPositionTexture(p2, new Vector2(0,0)); mirrorVertices[i++] = new VertexPositionTexture(p3, new Vector2(0,0)); mirrorPlane = new Plane(p0, p1, p2); }
当相机位置发生改变时,你需要更新mirrorViewMatrix和clipPlane变量,因为它们取决于普通View矩阵:
private void UpdateMirrorViewMatrix() { Vector3 mirrorCamPosition = MirrorVector3(mirrorPlane, fpsCam.Position); Vector3 mirrorTargetPosition = MirrorVector3(mirrorPlane,fpsCam.TargetPosition); Vector3 camUpPosition = fpsCam.Position + fpsCam.UpVector; Vector3 mirrorCamUpPosition = MirrorVector3(mirrorPlane, camUpPosition); Vector3 mirrorUpVector = mirrorCamUpPosition - mirrorCamPosition; mirrorViewMatrix = Matrix.CreateLookAt(mirrorCamPosition, mirrorTargetPosition, mirrorUpVector); }
private Vector3 MirrorVector3(Plane mirrorPlane, Vector3 originalV3) { float distV3ToPlane = mirrorPlane.DotCoordinate(originalV3); Vector3 mirroredV3 = originalV3 - 2 * distV3ToPlane * mirrorPlane.Normal; return mirroredV3; }
private void UpdateClipPlane() { Matrix camMatrix = mirrorViewMatrix * fpsCam.ProjectionMatrix; Matrix invCamMatrix = Matrix.Invert(camMatrix); invCamMatrix = Matrix.Transpose(invCamMatrix); Vector4 mirrorPlaneCoeffs = new Vector4(mirrorPlane.Normal, mirrorPlane.D); Vector4 clipPlaneCoeffs = Vector4.Transform(-mirrorPlaneCoeffs, invCamMatrix); clipPlane = new Plane(clipPlaneCoeffs); }
在绘制过程中,你首先将从镜像相机中看到的场景绘制到一张纹理中,然后清除屏幕,绘制从普通相机中看到的场景。之后绘制镜子:
protected override void Draw(GameTime gameTime) { //render scene as seen by mirror into render target device.SetRenderTarget(0, renderTarget); device.Clear(ClearOptions.Target | ClearOptions.DepthBuffer, Color.CornflowerBlue, 1, 0); device.ClipPlanes[0].Plane = clipPlane; device.ClipPlanes[0].IsEnabled = true; RenderScene(mirrorViewMatrix, fpsCam.ProjectionMatrix); device.ClipPlanes[0].IsEnabled = false; //deactivate custom render target, and save its contents into a texture device.SetRenderTarget(0, null); mirrorTexture = renderTarget.GetTexture(); //render scene + mirror as seen by user to screen graphics.GraphicsDevice.Clear(Color.Tomato); RenderScene(fpsCam.ViewMatrix, fpsCam.ProjectionMatrix); RenderMirror(); base.Draw(gameTime); }
镜子使用mirror technique绘制为一个简单的矩形,它的颜色来自于mirrorTexture变量:
private void RenderMirror() { mirrorEffect.Parameters["xWorld"].SetValue(Matrix.Identity); mirrorEffect.Parameters["xView"].SetValue(fpsCam.ViewMatrix); mirrorEffect.Parameters["xProjection"].SetValue(fpsCam.ProjectionMatrix); mirrorEffect.Parameters["xMirrorView"].SetValue(mirrorViewMatrix); mirrorEffect.Parameters["xMirrorTexture"].SetValue(mirrorTexture); mirrorEffect.Begin(); foreach (EffectPass pass in mirrorEffect.CurrentTechnique.Passes) { pass.Begin(); device.VertexDeclaration = new VertexDeclaration(device, VertexPositionTexture.VertexElements); device.DrawUserPrimitives<VertexPositionTexture> (PrimitiveType.TriangleStrip, mirrorVertices, 0, 2); pass.End(); } mirrorEffect.End(); }
对镜子的每个顶点,vertex shader计算2D屏幕位置和xMirrorTexture中对应的位置:
MirVertexToPixel MirrorVS(float4 inPos: POSITION0) { MirVertexToPixel Output = (MirVertexToPixel)0; float4x4 preViewProjection = mul(xView, xProjection); float4x4 preWorldViewProjection = mul(xWorld, preViewProjection); Output.Position = mul(inPos, preWorldViewProjection); float4x4 preMirrorViewProjection = mul (xMirrorView, xProjection); float4x4 preMirrorWorldViewProjection = mul(xWorld, preMirrorViewProjection); Output.TexCoord = mul(inPos, preMirrorWorldViewProjection); return Output; }
pixel shader使用将这个坐标除以其次坐标,将位置从 [–1,1] 渲染目标区间映射到[0,1]纹理坐标区间。使用结果纹理坐标,采样xMirrorTexture对应位置的颜色并返回:
MirPixelToFrame MirrorPS(MirVertexToPixel PSIn) : COLOR0 { MirPixelToFrame Output = (MirPixelToFrame)0; float2 ProjectedTexCoords; ProjectedTexCoords[0] = PSIn.TexCoord.x/PSIn.TexCoord.w/2.0f +0.5f; ProjectedTexCoords[1] = -PSIn.TexCoord.y/PSIn.TexCoord.w/2.0f +0.5f; Output.Color = tex2D(textureSampler, ProjectedTexCoords); return Output; }
发布时间:2009/10/14 上午8:50:53 阅读次数:9299