camera和viewport

乍一看我们只需要一个camera就够了。虽然说我们可以有很多不同的camera挂载在不同的场景节点上,我们也可以通过切换不同的camera很方便地切换视角,但同一时刻只有一个camera是在工作得。既然如此,我为何不能让一个camera同时“挂载”在多个场景节点上,然后通过切换父节点来改变视角呢?

当然是不行的啦。虽然大部分时候都是只有一个camera在工作,但还是有多情况是需要多个camera同时工作的,比如画中画,比如镜面反射(“分屏”这种这么奇葩的应用我就不说了)。似乎camera是一种很神奇的物种,我们可以用SceneManager创建出很多个camera,却无法命令他们同时工作,因此camera实际上和SceneManager关系不大。真正和camera存在密切联系的是viewport,一个camera是工作在一个viewport之上的。

在我的上一篇SceneNode, Movable和Renderable中有提到Ogre渲染场景的流程,实际上在renderScene()函数之上还有两层调用,这次为了更加明晰我把部分参数也写出来了。

Viewport::update()
    --> Camera::renderScene(Viewport*)
        --> SceneManager::renderScene(Camera*, Viewport*)

在每一层的函数调用中,调用者都会把自己当做参数传入下一层。可见场景的渲染同时需要camera和viewport的协作。

在这里还想提一下viewport和render target的关系。render target可以被理解为画布,而viewport则是作画的区域。显然作画区域再大也不能大得过画布。并且,当viewport尺寸小于render target的时候,内容会被压缩而不是被裁剪。我还没有尝试过大于的情况,我不过猜测那样会导致裁剪。

上图是在同一个窗口同时存在两个viewport的情况。(ps:大家会不会觉得右边的那个物体的角度和左边的那个不一样?哈哈,其实是视觉错觉,我专门打格子测量过,不行你也可以试试看)

无论是camera还是viewport,最终都会被转化为矩阵表现出来。三个基本矩阵中,WorldMatrix表示了物体在场景中的位置,剩下的两个矩阵ViewMatrix和ProjectionMatrix则与camera和viewport息息相关。下面分别列出了Dx11中创建这两个矩阵的API调用。

// view matrix
XMMatrixLookAtRH(XMVECTOR eyePos, XMVECTOR lookatPos, XMVECTOR upDirection)
// projection matrix
XMMatrixPerspectiveFovRH(float FOV, float aspectRatio, float nearZ, float farZ)

函数签名中的RH代表的含义是,所创建的矩阵符合右手坐标系。

XMMatrixLookAtRH()中三个参数的含义分别为摄像机位置,摄像机看向的目标以及摄像机向上的方向向量。第一个其实就是摄像机的父节点的坐标,第二和第三个可以从父节点的Orientation中得到。Orientation是由四元数表示的,一个四元数对应着唯一一个等价旋转矩阵(相反一个旋转矩阵对应着两个等价的四元数)。由于旋转矩阵的三列(或者是三行?)分别代表着节点当前朝向的xyz轴向量,因此camera的upDirection实际上就是y轴,lookPosision可以由z轴以及相机当前位置计算出来。至于四元数如何转换成旋转矩阵,我还是直接贴Ogre的源码吧。

//-----------------------------------------------------------------------
// 注:两个函数是Quaternion的成员函数,那些不知道从哪里冒出来的变量其实都是成员变量
    void Quaternion::ToRotationMatrix (Matrix3& kRot) const
    {
        Real fTx  = x+x;
        Real fTy  = y+y;
        Real fTz  = z+z;
        Real fTwx = fTx*w;
        Real fTwy = fTy*w;
        Real fTwz = fTz*w;
        Real fTxx = fTx*x;
        Real fTxy = fTy*x;
        Real fTxz = fTz*x;
        Real fTyy = fTy*y;
        Real fTyz = fTz*y;
        Real fTzz = fTz*z;

        kRot[0][0] = 1.0f-(fTyy+fTzz);
        kRot[0][1] = fTxy-fTwz;
        kRot[0][2] = fTxz+fTwy;
        kRot[1][0] = fTxy+fTwz;
        kRot[1][1] = 1.0f-(fTxx+fTzz);
        kRot[1][2] = fTyz-fTwx;
        kRot[2][0] = fTxz-fTwy;
        kRot[2][1] = fTyz+fTwx;
        kRot[2][2] = 1.0f-(fTxx+fTyy);
    }

 //-----------------------------------------------------------------------
    void Quaternion::ToAxes (Vector3* akAxis) const
    {
        Matrix3 kRot;

        ToRotationMatrix(kRot);

        for (size_t iCol = 0; iCol < 3; iCol++)
        {
            akAxis[iCol].x = kRot[0][iCol];
            akAxis[iCol].y = kRot[1][iCol];
            akAxis[iCol].z = kRot[2][iCol];
        }
    }

XMMatrixPerspectiveFovRH()函数的第一个参数FOV描述了相机的镜头,越大表示长焦,越小表示广角。第二个参数aspectRatio表示viewport的宽高比,一般而言使用窗口的宽除以窗口的高计算得出。最后两个参数描述了相机的近平面和远平面。相机的投影的景深是由FOV,zNear和zFar共同作用的结果。下面这幅图描述了这三者的关系。

——————————– 华丽分割线 ——————————–

本来今天准备把类的设计也写完的,但是今天被别人叫去写了一天java,真的是恶心死了。明天还要被人拉去写java啊。。。

使用DX11渲染的基本流程

总的来说,使用dx11渲染大体可分为四个过程。它们是Initialize(初始化),Load(加载),Render(渲染),Release(销毁)。实际上来说Initialize和Load可合并为一个过程,但习惯上我还是将它们分开对待。

Initialize
初始化我们的dx11设备,分为以下几个步骤。

1. 创建device(设备),context(上下文),swapchain(交换链)。使用D3D11CreateDeviceAndSwapChain函数可以同时创建这三者。其中device主要用于在Load过程中加载各种资源,context主要用于在Render过程中设置传入显卡的数据。swapchain描述了输出窗口,渲染帧率以及渲染目标(render target),同时还提供了双缓冲。

2. 创建深度缓冲区(depth buffer)。在dx11中深度缓冲是用2d纹理作为载体。使用device->CreateDepthStencilView函数。

3. 创建render target。render target其实可以理解为我们所有绘制行为的最终目的地,对于即时渲染来说这个目的地就是屏幕。因此render target可以用一张2d纹理来描述,我们只是不断地往这个画布上作画。这一步使用device->CreateRenderTargetView函数。

4. 使用context->OMSetRenderTargets将该render target设置为向屏幕输出。

5. 创建视口(viewport)。使用context->RSSetViewports设置视口。

Load
加载各种资源。事实上,Initialize和Load都是在Render之前执行一次的过程。之所以将他们分开,是因为Initialize中的行为在绝大部分情况下仅执行一次,而在Load中我们极有可能大量且重复地加载资源。以下是一些基本的需要加载的资源。

1. Shader文件或Effect文件。旧时代中(DX9-, OpenGL)Shader是非必要的,然而dx11移除了固定管线,意味着shader这东西成了必需品,要“人手一份”。

2. input layout(输入布局?)。当我们向GPU传递顶点时,我们可以自定义一个Vertex由哪些部分组成。一些必要的部分包括坐标,纹理坐标,法向量。由于Vertex结构是由我们自己定义的,所以我们有义务告诉GPU该结构的布局。这些布局的信息就在 input layout 中给出。

3. vertex buffer(顶点缓冲)。这些可爱的小豆豆可以由我们手动指定,或者从一个外部3D模型中导入。当然,我们的vertex结构要与 input layout 中描述的保持一致。

4. texture(纹理)。

5. 其他要传入显卡的数据。

Render
永不停息地奔跑,以我们设定的帧率不停地绘制的过程。在我看来,dx11中的Render过程实际上是CPU不断地将数据输入GPU的过程。在CPU中,这个过程是单线程的。我们通过不断切换context(上下文)的各种数据,组装出各种物件,并将他们传入GPU。基本上Render过程由以下几个步骤组成。

1. 清空 back buffer 和 depth buffer。
————————这条线以上的仅做一次 这条线以下的有多少东西就要做多少次——————————
2. 指定要输入的顶点信息。这个过程由contex->IASetInputLayout, contex->IASetVertexBuffer 和 context->IASetPrimitiveTopology 三个函数的调用完成。三个函数分别设置了顶点结构布局,顶点缓冲和图元(绘制顶点的方法,像 triangle list 那种东西)。

3. 设置纹理。

4. 设置需要应用的shader或effect。

5. 调用context->draw方法,将数据提交给显卡。
————————这条线以下的仅做一次 这条线以上的有多少东西就要做多少次——————————
6. 调用swapchain->present方法,交换双缓冲。

Release
释放掉所有东西。

Q&A
Q:世界矩阵,视图矩阵和投影矩阵呢?灯光呢?这些东西到哪里去了?
A:这些玩意随着旧世界的固定管线一并消失了,现在你需要在shader中手动完成这些运算。简而言之,要将灯光和矩阵的数据传入GPU,然后在shader中将他们与顶点做运算。

Q:好像少了很多东西,dx11并不止这些。
A:是的,但我仅仅把最基础最必要的流程列了出来,是为了留给自己一个清晰的思路。

初接触DX11

首先要吐槽一下这本鸡巴书《Beginning DirectX 11 Game Programming》。我真是昏了头才去亚马逊买了正版,花了我两百块钱啊!买回来一看居然还没有OpenGL蓝宝书厚。翻开一看尼玛这什么排版,所有字体几乎都一样也就算了,居然字号那么大,行距那么开,坑钱也要有个限度吧?

以上是碎碎念。既然书都买回来了,自然要好好学习。DX11开发环境倒是很容易弄,只要电脑上装了VS2010,再去官网下载个最新版的DX11SDK就行了。
我把我的学习项目给上传到了git,理论上只需要从这个地址Clone
https://github.com/kidsang/SaberCore.git
甚至都不需要装DX11的SDK,我的git中已经包含了DX11开发的必要文件了。当然项目引用目录什么的要自己设置下。

DX11相对于OpenGL也好,相对于更早的DX9也好,最大的不同在于它完全移除了固定管线。因此以前那种使用C++硬编码这里放个光源那里贴个纹理的日子一去不复返了,现在一切的计算都移到了Shader中要自己手动写。坏处在于对于一个对图形学什么都没接触过的新手来说学习门槛更高了;好处在于这么做极大简化了DirectX的模型,并且相对于固定管线,可编程管线更加灵活且威力更大。

(吐槽一下微软的变量名和宏定义,尼玛一个枚举类型D3D11_FILTER_COMPARISON_MIN_MAG_LINEAR_MIP_POINT这么鸡巴长还要大写数字下划线混合还要一大堆长得几乎一样的尼玛一个词前面五分之四都相同就后面两个词尾不同整个程序都是这种恶心玩意一坨一坨打字的我手抽筋)

由于没有了固定管线,所以什么东西都要向显卡传,甚至是最基本的顶点数据。这是和OpenGL最主要的不同之一。在OpenGL中我们可以很愉快地在glBegin()和glEnd()之间编写顶点,但在DX11中就行不通了。因为我们的顶点是要作为数组传给显卡的,因此除了定义顶点外还要告诉显卡我们的顶点数据结构布局。实际上,DX11在C++中的硬编码大部分都是在定义各种要传给显卡的数据的描述,真正的图形处理逻辑大部分都是使用HLSL在Shader中完成的。

由于Shader承担着DX11重要的任务,因此其管线变得颇为复杂。在我用的OpenGL版本中,Shader就两道工序,一个顶点Shader一个片段Shader。在DX11中的管线模型是这样的:
Input Assembler -> Vertex Shader -> Hull Shader -> Tessellator -> Domain SHader -> Geometry Shader -> Rasterizer -> Pixel Shader -> Output Merger
从中我们可以看得到茫茫多的shader,其中的大部分我依旧不知道是干嘛用的,不过随着我的进一步学习应该会慢慢明晰。HLSL的语法倒是和GLSL颇为相似,这是个好事情。

另外,我至今不明白为何微软一定要把纹理坐标的Y轴翻转过来。