zoukankan      html  css  js  c++  java
  • 在3D世界中创建不同的相机模式——创建一个相机:Position,Target和View Frustum

    2.1 创建一个相机:Position,Target和View Frustum

    问题

    在将3D世界绘制到屏幕之前,你需要相机的View和Projection矩阵。

    解决方案

    你可以在一个矩阵中保存相机位置和方向,这个矩阵叫做View矩阵(视矩阵,观察矩阵)。要创建View矩阵,XNA需要知道相机的Position,Target和Up矢量。

    你也可以保存视锥体(view frustum),它是3D世界中实际可见的部分,在另一个叫做Projection的矩阵中。

    工作原理

    View矩阵保存相机位置和观察方向的定义。你可以通过调用Matrix. CreateLookAt方法创建这个矩阵:

    viewMatrix = Matrix.CreateLookAt(camPosition, camTarget, camUpVector); 

    这个方法需要三个参数:相机的Position, Target和Up向量。Position向量容易理解,它表示在3D空间中的哪个位置放置相机。然后,需要指定另一个点表示相机观察的目标。这已经可以定义一个相机了,但Up向量用来干什么?

    看一下这个例子:你的头(事实上是你的眼睛)就是相机。你试着定义一个与头有相同位置和方向的相机。第一个向量很容易找到:Position向量就是头在3D场景中的位置。然后, Target向量也不是很难;假设你看着图2-1中的X,在这种情况中,X的位置就是相机的Target 向量。但有其他方式可以让在同一位置的头看着X!

    1

    图2-1 相机的观察目标

    只定义了Position和Target向量 ,你也可以绕着双眼之间的点旋转头部,例如,上下颠倒看。如果你这样做,头部的位置和观察目标仍保持不变,但因为所有东西都进行了旋转,观察到的图像会完全不同。你就是为什么需要定义相机的Up向量的原因。

    知道了相机的位置,观察目标和相机的up方向,相机就唯一确定了。View矩阵由这三个向量决定,可以使用Matrix. CreateLookAt方法创建一个相机:

    Matrix viewMatrix; 
    Vector3 camPosition = new Vector3(10, 0, 0); 
    Vector3 camTarget = new Vector3(0, 0, 0); 
    Vector3 camUpVector = new Vector3(0, 1, 0); 
    viewMatrix = Matrix.CreateLookAt(camPosition, camTarget, camUpVector); 

    注意:相机的Position和Target向量指向3D空间中的真实位置,Up向量表示相机向上方向。例如,一个相机位于点(300,0,0)观察点(200,0,0)。如果相机的Up向量只是简单地向上,只需指定(0,1,0)Up向量,这不是指在3D空间中的点,这个例子中这个3D点为(300,1,0)。

    注意:XNA为最常用的向量提供了一个快捷方式,Vector3. Up表示(0,1,0),Vector3. Forward表示(0,0,-1),Vector3. Right表示(1,0,0)。为了帮你理解3D向量,本章第一个教程都使用完整的写法。

    XNA还需要Projection矩阵。你可以将这个矩阵看成可以映射从3D空间到2D窗口的所有点的一个东西,但我更希望你把它看成包含相机镜头信息的矩阵。

    让我们看一下图2-2,左图显示了一个在相机视野中的3D场景,你可以看到它像一个金字塔。右图中你可以看到金字塔的一个2D切面。

    2

    图2-2 相机的视锥体

    图片左边的切除顶部的金字塔叫做视锥体(view frustum)。只有在视锥体内部的物体才会被绘制到屏幕上。

    XNA可以为你创建这样一个视锥体,它存储在Projection矩阵中。你可以调用Matrix. CreatePerspectiveFieldOfView创建这个矩阵:

    projectionMatrix = Matrix.CreatePerspectiveFieldOfView(viewAngle, aspectRatio, nearPlane, farPlane);

    Matrix. CreatePerspectiveFieldOfView方法的第一个参数是观察角度。它对应金字塔顶角的一半,如图2-2右图所示。如果你想知道自己的观察角度,可以将手放在眼睛前面,你会发现这个角度约为90度。因为弧度PI等于180度,90等于PI/2。因为你需要指定观察角度的一半,所以这个参数为PI/4。

    注意:通常你想使用一个对应人的视角的视角,但是在某些场景中你可能会指定其他的视角。通常发生在你想将场景绘制到一张纹理的情况中,例如,从光线的视角看来。在光线的情况中,更大的视角表示更大的光照范围。对应的例子可参见教程3-13。

    你需要指定的另一个参数与“source,”无关,即与视锥体无关,而和“destination,”有关,即与屏幕有关。它是2D屏幕的长宽比,它实际上对应后备缓冲的长宽比,你可以使用以下代码获取它:

    float aspectRatio = graphics.GraphicsDevice.Viewport.AspectRatio; 

    当使用一个长和宽相同的正方形窗口时,这个比为1。但是绘制全屏800 × 600窗口时这个比大于1,当绘制到宽屏笔记本或HDTV更大。如果你用错误地用1代替800/600作为800*600窗口的长宽比,图像会水*拉伸。

    最后两个参数与视锥体有关。想象一下一个物体非常靠*相机,这个物体会占据整个视野,窗口会被一个单独的颜色占满。要避免这个情况的发生,XNA让你可以定义一个靠*金字塔顶部的*面。位于金字塔顶部和这个*面之间的物体不会被绘制,这个*面叫做*裁*面(near clipping plane),你可以指定相机到这个*裁*面的距离作为CreatePerspectiveFieldOfView方法的第三个参数。

    注意:剪裁过程用来表示有些物体无需被绘制以提高程序帧频率。

    同理也可以处理离相机非常远的物体;这些物体看起来很小,但仍占用显卡的处理时间。所以,远于第二个*面的物体也会被剪裁。第二个*面叫做远裁*面(far clipping plane),它是视锥体的最远边界。你可以指定相机到这个远裁*面的距离作为CreatePerspectiveFieldOfView方法的最后一个参数。

    当心:即使绘制的是一个及其简单的3D场景,也不要把远裁*面设置地过大。例如将远裁*面的距离设置为比较疯狂的100000会导致一些视觉错误。带有16-bit深度缓冲的显卡(可参见本教程的“Z-Buffer (或Depth Buffer)”一节)有2^16 = 65535个深度值。如果两个物体使用同一个像素,而且之间的距离小于100k/65535 = 1.53个单位时,显卡就无法判断哪个物体更加靠*相机。

    事实上,这会导致更坏的结果,因为scale is quadratic(?),会导致整个场景的最后三个 quarters(?)看起来离开相机的距离相同。*裁*面和远裁*面间的距离最好小于几百,如果显卡的深度缓冲小于16-bit,这个距离应该更小。

    这个问题的典型错误就是你看到的所有对象都有锯齿边缘。

    使用方法

    你想在程序的更新过程中更新View矩阵,因为相机的位置和方向是基于用户输入的。Projection矩阵只需在窗口的长宽比发生变化时才需要更新,例如,当将窗口切换到全屏模式时。

    计算好View和Projection矩阵之后,你需要将它们传递到绘制物体的effect中,可在下面的Draw方法中看到对应代码。这可以让显卡上的shader将所有的顶点转换为窗口的对应像素。

    代码

    下面的例子显示了如何创建一个View矩阵和一个Projection矩阵。比方说你有一个物体位于(0,0,0),你想将相机放置在x轴上+10个单位的地方,正y轴作为Up向量。 而且,你想在800 × 600窗口中绘制场景,使所有与相机的距离小于0.5f大于100.0f的三角形被剪裁。下面是代码:

    using System; 
    using System.Collections.Generic; 
    using Microsoft.Xna.Framework; 
    using Microsoft.Xna.Framework.Audio; 
    using Microsoft.Xna.Framework.Content; 
    using Microsoft.Xna.Framework.Graphics; 
    using Microsoft.Xna.Framework.Input; 
    using Microsoft.Xna.Framework.Storage; 
    
    namespace BookCode 
    {
        public class Game1 : Microsoft.Xna.Framework.Game 
        { 
            GraphicsDeviceManager graphics; 
            ContentManager content; 
            BasicEffect basicEffect; 
            GraphicsDevice device; 
            
            CoordCross cCross; 
            Matrix viewMatrix; 
            Matrix projectionMatrix; 
            
            public Game1() 
            {
                graphics = new GraphicsDeviceManager(this); 
                content = new ContentManager(Services); 
            } 

    只有在窗口的长宽比发生变化时才需要更新Projection矩阵。你只需要定义一次Projection矩阵,所以放在程序的初始化过程中。

    protected override void Initialize() 
    {
        base.Initialize(); 
        float viewAngle = MathHelper.PiOver4; 
        float aspectRatio = graphics.GraphicsDevice.Viewport.AspectRatio; 
        float nearPlane = 0.5f; 
        float farPlane = 100.0f; 
        
        projectionMatrix = Matrix.CreatePerspectiveFieldOfView(viewAngle, aspectRatio, nearPlane,farPlane); 
    }
    
    protected override void LoadContent() 
    {
        device = graphics.GraphicsDevice; basicEffect = new BasicEffect(device, null); 
        
        cCross = new CoordCross(device); 
    }
    
    protected override void UnLoadContent() 
    {
    } 

    你需要改变View矩阵让用户输入移动相机,因此将它放在程序的更新过程中。

    protected override void Update(GameTime gameTime) 
    {
        if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) 
            this.Exit(); 
        
        Vector3 camPosition = new Vector3(10, 10, -10); 
        Vector3 camTarget = new Vector3(0, 0, 0); 
        Vector3 camUpVector = new Vector3(0, 1, 0); 
        viewMatrix = Matrix.CreateLookAt(camPosition, camTarget, camUpVector); 
        
        base.Update(gameTime); 
    } 

    然后将Projection和View矩阵传递到Effect绘制场景:

    protected override void Draw(GameTime gameTime) 
    { 
        graphics.GraphicsDevice.Clear(Color.CornflowerBlue); 
        
        basicEffect.World = Matrix.Identity; 
        basicEffect.View = viewMatrix;
        basicEffect.Projection = projectionMatrix;
        
        basicEffect.Begin(); 
        foreach (EffectPass pass in basicEffect.CurrentTechnique.Passes) 
        {
            pass.Begin(); 
            cCross.DrawUsingPresetEffect(); 
            pass.End(); 
        }
        basicEffect.End(); 
        
        base.Draw(gameTime); 
    } 
    扩展阅读

    只需要用到前面两个矩阵XNA就可以将3D场景绘制到2D屏幕中。从3D转换到2D是个挑战,但XNA已经帮你做到了。但是,要创建和调试更大的3D程序需要深入理解这个操作背后到底发生了什么事。 Z-Buffer (或Depth Buffer) 第一个挑战是指定哪个物体会占据最终图像上的像素。当从3D空间转换到2D屏幕时,有可能多个物体都显示在同一个像素上,如图2-3所示。2D屏幕上的一个像素对应3D空间中的一条射线,解释可参见教程4-14。对一个像素,这条射线如图2-3中的虚线所示,它与两个物体相交。这种情况中,这个像素的颜色会取自物体A,因为A比B更靠*相机。

    3

    图2-3 多个物体占据同一个像素

    但是,如果首先绘制物体B,frame buffer中对应像素会首先被指定为B的颜色。然后物体A被绘制,显卡需要判断像素是否需要用物体A的颜色覆盖。

    解决方法是,在显卡中还储存了第二张图像,它的大小与窗口大小一样。当给frame buffer中的一个像素指定一个颜色时,这个物体和相机间的距离会保存在第二个图像中。这个距离介于0和1之间,0对应*裁*面与相机间的距离,1对应远裁*面与相机间的距离。所以第二个图像叫做depth buffer或z-buffer。

    那么如何解决这个问题?当绘制物体B时会检查z-buffe,因为B首先绘制,z-buffer是空的。结果是frame buffer中对应像素的颜色就是B的颜色,在z-buffer的相同像素中获得一个值,对应B物体与相机间的距离。

    然后绘制物体A,对应物体A的每个像素,首先检查z-buffer。z-buffer已经包含了物体B的值,但储存在z-buffer中的距离大于物体A与相机间的距离,所以显卡知道需要用物体A的颜色覆盖这个像素!

  • 相关阅读:
    matplotlib 进阶之origin and extent in imshow
    Momentum and NAG
    matplotlib 进阶之Tight Layout guide
    matplotlib 进阶之Constrained Layout Guide
    matplotlib 进阶之Customizing Figure Layouts Using GridSpec and Other Functions
    matplotlb 进阶之Styling with cycler
    matplotlib 进阶之Legend guide
    Django Admin Cookbook-10如何启用对计算字段的过滤
    Django Admin Cookbook-9如何启用对计算字段的排序
    Django Admin Cookbook-8如何在Django admin中优化查询
  • 原文地址:https://www.cnblogs.com/AlexCheng/p/2120176.html
Copyright © 2011-2022 走看看