【图形学与游戏编程】开发笔记-入门篇3:图形绘制

来源:互联网 发布:java调用python程序 编辑:程序博客网 时间:2024/05/29 09:40

(本系列文章由pancy12138编写,转载请注明出处:http://blog.csdn.net/pancy12138)

这篇文章将会开始讲解最基本的图形绘制方法,也就是说。这一次的教程将为大家展示一个3D图形是怎么被一步步的处理并最终显示出来的。当然,大家应该还记得入门篇所提到的渲染管线以及数学,程序等基础知识吧,这一节就是要教会大家利用这些知识来构建3D场景。

首先,我们来回忆一下渲染管线的第一步,很明显第一步应该是先描述出一个几何体出来,也就是要告诉计算机我们希望它画什么,那么如何才能把一个几何体描述出来呢,在入门篇教程中我们已经解释过了,图形学是用一个个的网格来描述一个几何体。当然,作为刚开始的教程,我们先来描述一个比较简单的网格,也就是一个简单的立方体。对于一个立方体而言,大家都很清楚他的顶点一共只有八个。然后面的话有六个四边面,每个四边面由两个三角形组成,也就是说最终有12个面。我们描述一个三角形需要3个点,也就是说需要36个点来描述着六个面。下面将给出一个立方体在内存中的存储代码,注意这个代码里面立方体的顶点并不是我们刚刚说的8个,这是因为这个立方体拥有纹理坐标,因此有些点虽然位置一样(比如都是(0,0,1)),但是纹理坐标不一样,必须要拆成两个点。关于神马是纹理我们会在之后讲,这节课主要是讲几何体的绘制,这些东西可以先放一放。大家知道有些点是重复的就好了:

struct pancy_point{XMFLOAT3 position;XMFLOAT3 normal;XMFLOAT2 tex;};pancy_point square_test[]={{XMFLOAT3(-1.0f,1.0f,-1.0f), XMFLOAT3(-1.0f,1.0f,-1.0f), XMFLOAT2(0.0f,0.0f)},{XMFLOAT3(1.0f,1.0f,-1.0f),  XMFLOAT3(1.0f,1.0f,-1.0f),  XMFLOAT2(1.0f,0.0f)},{XMFLOAT3(1.0f,-1.0f,-1.0f), XMFLOAT3(1.0f,-1.0f,-1.0f), XMFLOAT2(1.0f,1.0f)},{XMFLOAT3(-1.0f,-1.0f,-1.0f),XMFLOAT3(-1.0f,-1.0f,-1.0f),XMFLOAT2(0.0f,1.0f)},{XMFLOAT3(1.0f,1.0f,1.0f),   XMFLOAT3(1.0f,1.0f,1.0f),   XMFLOAT2(0.0f,0.0f)},{XMFLOAT3(-1.0f,1.0f,1.0f),  XMFLOAT3(-1.0f,1.0f,1.0f),  XMFLOAT2(1.0f,0.0f)},{XMFLOAT3(-1.0f,-1.0f,1.0f), XMFLOAT3(-1.0f,-1.0f,1.0f), XMFLOAT2(1.0f,1.0f)},{XMFLOAT3(1.0f,-1.0f,1.0f),  XMFLOAT3(1.0f,-1.0f,1.0f),  XMFLOAT2(0.0f,1.0f)},{XMFLOAT3(-1.0f,1.0f,1.0f),  XMFLOAT3(-1.0f,1.0f,1.0f),  XMFLOAT2(0.0f,0.0f)},{XMFLOAT3(1.0f,1.0f,1.0f),   XMFLOAT3(1.0f,1.0f,1.0f),   XMFLOAT2(1.0f,0.0f)},{XMFLOAT3(1.0f,1.0f,-1.0f),  XMFLOAT3(1.0f,1.0f,-1.0f),  XMFLOAT2(1.0f,1.0f)},{XMFLOAT3(-1.0f,1.0f,-1.0f), XMFLOAT3(-1.0f,1.0f,-1.0f), XMFLOAT2(0.0f,1.0f)},{XMFLOAT3(-1.0f,-1.0f,-1.0f),XMFLOAT3(-1.0f,-1.0f,-1.0f),XMFLOAT2(0.0f,0.0f)},{XMFLOAT3(1.0f,-1.0f,-1.0f), XMFLOAT3(1.0f,-1.0f,-1.0f), XMFLOAT2(1.0f,0.0f)},{XMFLOAT3(1.0f,-1.0f,1.0f),  XMFLOAT3(1.0f,-1.0f,1.0f),  XMFLOAT2(1.0f,1.0f)},{XMFLOAT3(-1.0f,-1.0f,1.0f), XMFLOAT3(-1.0f,-1.0f,1.0f), XMFLOAT2(0.0f,1.0f)},{XMFLOAT3(-1.0f,1.0f,1.0f),  XMFLOAT3(-1.0f,1.0f,1.0f),  XMFLOAT2(0.0f,0.0f)},{XMFLOAT3(-1.0f,1.0f,-1.0f), XMFLOAT3(-1.0f,1.0f,-1.0f), XMFLOAT2(1.0f,0.0f)},{XMFLOAT3(-1.0f,-1.0f,-1.0f),XMFLOAT3(-1.0f,-1.0f,-1.0f),XMFLOAT2(1.0f,1.0f)},{XMFLOAT3(-1.0f,-1.0f,1.0f), XMFLOAT3(-1.0f,-1.0f,1.0f), XMFLOAT2(0.0f,1.0f)},{XMFLOAT3(1.0f,1.0f,-1.0f),  XMFLOAT3(1.0f,1.0f,-1.0f),  XMFLOAT2(0.0f,0.0f)},{XMFLOAT3(1.0f,1.0f,1.0f),   XMFLOAT3(1.0f,1.0f,1.0f),   XMFLOAT2(1.0f,0.0f)},{XMFLOAT3(1.0f,-1.0f,1.0f),  XMFLOAT3(1.0f,-1.0f,1.0f),  XMFLOAT2(1.0f,1.0f)},{XMFLOAT3(1.0f,-1.0f,-1.0f), XMFLOAT3(1.0f,-1.0f,-1.0f), XMFLOAT2(0.0f,1.0f)},};//创建索引数组。num_vertex = sizeof(square_test) / sizeof(pancy_point);for(int i = 0; i < num_vertex; ++i){vertex[i] = square_test[i];}UINT indices[] = {0,1,2, 0,2,3, 4,5,6, 4,6,7, 8,9,10, 8,10,11, 12,13,14, 12,14,15, 16,17,18, 16,18,19, 20,21,22, 20,22,23};
上面的代码展示了在CPU上创建两个数组来存储一个几何体网格的顶点以及索引的方法,首先我们来看顶点,顶点是一个网格最主要的信息存储方式,我们不仅仅需要在这个数据结构里面存储每个点的位置,相应的一些别的属性,例如法向量,纹理坐标,切线等我们都需要存储在这个结构体里面。之前我们提过,整个图形学程序的基础是线性代数,也就是说我们的大部分变量不再是大家以前写C/C++/java这些语言的时候所用的int啊,float啊这些简单的变量,而是 换做float3啊,float4啊,matrix啊这些变量。至于原因大家也很容易看的出来,比如说这里我们要描述“坐标”这个概念,这个概念肯定是一个三维的变量,至少得三个float才能存的下来。注意我们这里顶点是用一个"pancy_point"结构体来存储的。这个结构是我自己定义的,大家也可以随便定义一个结构来描述一个点,只不过内部所用的变量都是一些XMFLOAT的directx内置向量类型而不是int啊float啊这些类型。当然啦,肯定有人会问难道我这里随便定义一个结构体,directx都能认得出来我定义的点吗?答案显然是肯定的,因为你这里定义的点,最终渲染的时候还是你自己用,其实directx是不管的,这个待会大家学完这一篇就知道了。然后我们来看索引,索引就很简单了,三个数字代表一个三角形,比如前三个0,1,2意思就是第0个点,第一个点,第二个点组成了一个三角形,这里的第几个点指的就是之前的那个顶点缓冲区的点。当然,索引是用于光栅化三角面的,这个类型固定的就是UINT,然后最终渲染的时候就不归大家管了,属于不可编程单元。这里我们提一下,依靠索引号来标识三角面并不是唯一的方法,像曲面细分就不依靠这种办法来标识三角面,当然啦,曲面细分的光栅化过程也属于不可编程的单元。这个我们回头再讲。

上面的代码相信大家一眼都能看懂,因为这就是普通的数组操作,跟我们平常写的黑框框一样,甚至都没用到神马API。下面我们再讲点重要的,很明显,大家创建的这两个数组现在还是在CPU上的,我们下一步就是要想办法把这个CPU上的网格(也就是两个数组),送到GPU里面,这样我们才能对他进行变换啊,着色啊,绘制啊这些高运算量的操作。而directx的作用这个时候就体现出来了。首先,我用最通俗的语言来讲接下来的操作,directx可以在GPU上开数组资源,然后把数据复制过去,并且保留一个CPU上的指针来控制这个资源。通过这种方法,我们就可以灵活的控制GPU上的资源。那么现在回过头来我们看这个网格,很明显这个网格需要在GPU上开两个数组存起来,因此我们创建两个directx的资源指针,然后把数据通过这两个指针拷贝到GPU的显存里面:

D3D11_BUFFER_DESC point_buffer;point_buffer.Usage = D3D11_USAGE_IMMUTABLE;            //顶点是gpu只读型point_buffer.BindFlags = D3D11_BIND_VERTEX_BUFFER;         //缓存类型为顶点缓存point_buffer.ByteWidth = all_vertex * sizeof(T); //顶点缓存的大小point_buffer.CPUAccessFlags = 0;point_buffer.MiscFlags = 0;point_buffer.StructureByteStride = 0;D3D11_SUBRESOURCE_DATA resource_vertex;resource_vertex.pSysMem = vertex;//指定顶点数据的地址  //创建顶点缓冲区HRESULT hr = device_pancy->CreateBuffer(&point_buffer, &resource_vertex, &vertex_need);if (FAILED(hr)){MessageBox(0, L"init point error", L"tip", MB_OK);return hr;}//创建索引缓冲区D3D11_BUFFER_DESC index_buffer;index_buffer.ByteWidth = all_index*sizeof(UINT);index_buffer.BindFlags = D3D11_BIND_INDEX_BUFFER;index_buffer.Usage = D3D11_USAGE_IMMUTABLE;index_buffer.CPUAccessFlags = 0;index_buffer.MiscFlags = 0;index_buffer.StructureByteStride = 0;//然后给出数据D3D11_SUBRESOURCE_DATA resource_index = { 0 };resource_index.pSysMem = index;//根据描述和数据创建索引缓存hr = device_pancy->CreateBuffer(&index_buffer, &resource_index, &index_need);if (FAILED(hr)){MessageBox(0, L"init point error", L"tip", MB_OK);return hr;}
上面展示了如何创建GPU上的资源,首先对于buffer型的数组,directx提供了一种ID3D11Buffer*的指针(如果大家了解C++的基础的话这里我提一下,其实所有的类似GPU资源指针的类都是继承于一个叫resource的类,这个buffer类以及之后的texture类都是一样的),也就是上面的vertex_need与index_need这两个指针来指向GPU上的资源(当然这根CPU上的指针指向不是一个概念,不过这里这么讲比较形象),注意GPU上的资源数组比CPU上的数组要严格的多,因此我们在创建这个资源的时候要指定资源的格式,这里格式包括一些用途,绑定用途,大小啊,CPU可读性啊这些,这些格式将决定这个数组的大小,是否能读写,是否可以map到cpu上等等一系列的属性。大家以后将会经常用到,这里就不细讲了。我们可以看到借助d3d设备的CreateBuffer函数,我们就可以轻易的创建一片GPU上的数组,这里的三个参数分别是(数组格式,CPU上的资源指针,GPU上的资源指针),注意这个CPU上的资源指针不能直接用之前创建数组的那个指针,要先创建一个D3D11_SUBRESOURCE_DATA的指针,然后把之前那个CPU上的数组指针赋给这个指针的pSysMem变量。用这个新建的指针来作为CPU上的资源指针。这里大家也许发现了,这个GPU上的资源在创建的时候就必须要确定每个变量的值了,而不像CPU上的数组一般先开空间,在后面随意改值。这里我解释一下,之前我说directx通过一个buffer指针来控制GPU上的变量,而实际上,它并不能算是“控制”变量,也就是说这个指针不足以访问GPU上的数据的具体的值,他只是“宏观”的控制这个变量,至于怎么个宏观法大家之后写多了程序就能体验到了。如果大家想要改变这个GPU上的数组也是可以的,只不过这需要进行常说的map操作把数组从GPU上拷贝回CPU,这种操作非常的耗时间,大家能不改尽量不要改。也就是说在程序正式渲染之前,最好能把资源都确定好了然后全部存在GPU上。

通过上面的方法,我们就已经成功的在GPU的显存里面存储了一个几何体。通封装过这种方法,我们可以很轻松的在GPU上把场景中的所有几何体都变成两个两个的数组存储起来。接下来我们要讲的就是当我们把几何体存储完毕了之后,如何将这些几何体绘制出来了。我们千辛万苦的把几何体描述出来,然后又拷贝到GPU里面,为的就是把它能快速的渲染出来。渲染管线的基础知识我们在之前的入门篇讲过了,大家应该也有一个基本的认识了,这里呢我就再重复一遍给大家加深一下印象敲打。要想把它绘制出来,首先我们要做的就是先把它在3D空间的位置给定下来,虽然之前我们在数组中已经指定了这个立方体的每个点的坐标,但是一般我们要根据情况把这个立方体摆放到合适的3D空间中,这就需要对这个几何体进行最基本的几何变换,也就是大家通常所了解的平移,旋转,缩放。那么如何才能达到这个效果呢,肯定有人就想到写一个函数了。当然这个我们也在入门篇提到了,线性代数的优点就是利用矩阵乘法来代替函数。因此这里我们其实是使用三个基本的矩阵(平移,旋转,缩放)来代替函数做这些操作的。当我们通过点和矩阵相乘得到了变换后的几何体后,我们就可以进行下一步的操作了,也就是投影。这个操作将所有的三维点,通过一个矩阵,直接转换成二维的坐标(范围是(-1,-1)->(1,1)),这个投影矩阵的推导还是比较复杂的,不过因为directx默认永远在坐标原点朝着z轴正方形投影,因此,推导过程被化简了很多。当然,有人就问了,你这directx默认的投影方向就只有一个,那我想换视角怎么办,我想看场景的另一个方向怎么破?嘿嘿,科学家们当初想到了一个非常巧妙的方法解决了这个问题,并且给这个方法起了个名字叫“取景变换”,简单的来说,如果你想看场景的后面,你也不需要动,让整个场景转过来把背面朝着你就可以了。具体的方法我们在之后讲三维摄像机的时候会再提,这里大家先把他放过去,等讲到的时候再说生气。当投影完毕之后,我们就得到了一个二维的矢量图,这也同时标志着3D层面的工作到此结束,之后的工作都是二维层面上的工作了。接下来的紧接着的工作就是光栅化了,这个工作就是把投影之后的二维矢量图转换成二维的光栅图,说的通俗一点,就是把一个个用顶点表示的三角形(比如(1,0),(-1,0),(0,1)这种的),通过索引号以及屏幕像素点的位置来转换成像素点标识的三角形。我们知道矢量三角形一般依靠三个顶点来表示,而像素化的三角形就得用很多很多像素点来标识了,至于用多少像素点就看这个三角形有多大了,它能包住多少屏幕像素点就需要多少个像素点来标识。那么至于如何把一个个二维的矢量三角形转换成光栅化的三角形,这是光栅化硬件做的工作,也就是说我们程序员不需要管,大家如果有兴趣的话可以看看一些图形学课本,上面会提及一些光栅化所使用的填充算法以及扫描算法等等。我们接下来要关注的是光栅化完毕了之后我们要干神马,注意之前我们只提到了物体的形状怎么表示,怎么投影以及怎么转化为二维光栅图,但是并没有提及光栅化之后每个三角面的颜色应该是神马。也就是说接下来的操作就是为这些光栅化之后的三角形进行着色操作。这个时候有人就有问题了,因为之前我们在入门篇说过图形学的颜色都是依据光照原理得到的,也就是说根据物体和光源在3D空间之中的关系得到的,那么我们现在整个3D图形都已经经过了投影+光栅化成为了一个图片了,这图片怎么能有3D信息,怎么计算光照和颜色呢?这个问题其实很简单,在我们投影+光栅化的时候,每个顶点虽然被投影变换矩阵变成了二维的矢量点,然后又根据填充算法变成了一个个的光栅点。但是我们仍然可以为这些点保留它以前的信息,原因就是这些点是由“结构体”来表示的,而这个“结构体”是我们制定的,所以想保留多少信息就可以保留多少信息,我们只需要在这个过程中把结构体中代表坐标的部分进行变换就可以了,其余的完全可以保留到最后一步再进行计算。当然这个时候有人又会有疑问了,你投影的时候保留信息我相信,因为就是三个点变成三个点,但是你光栅化保留信息是神马鬼,一个三个点的三角形光栅化之后一般都会变成成千上万个像素点的啊,那除了三个顶点的信息以外,三角形里面的像素点的信息难道也是保留来的,哪来的数据给他们保留啊?那么这个问题就比较有深意了,其实里面的点的信息,也是保留下来的,我们知道光栅化是根据三个顶点的位置为三角形决定包住了多少个像素点。但是,这一步其实远不是那么简单,事实上他不仅仅把三角形矢量转换成了三角形像素点,他还为每个像素点通过“插值”算法得到了这个点所继承的结构体数据。那么神马是插值算法呢?很简单,比如说我们着色的时候需要用到顶点结构体里面法向量的信息,现在矢量三角形的三个顶点是a,b,c。现在我们想得到三角形的中心点的法向量,那么先进行线性插值得到两条边的中点的法向量信息n1 = (a+b)/2和n2 = (a+c) / 2。然后根据这两个边的中点的信息再得到中心点的信息(n1+n2) / 2。这样就插值得到了中点的法向量,这种算法就叫双线性插值算法。


当然,插值算法有很多种,并且这一步是不需要程序员们插手的,也就是说他会在光栅化的时候自动的为每一个像素点根据之前的结构体信息插值得到新的信息。这里就不得不提一下我们为什么要在光栅化之后着色了,注意有些人可能想到了在顶点还没有被投影的时候给每个顶点先把颜色算出来,然后再进行光栅化,这样岂不是不需要再记录神马原始信息了,光栅化的时候会根据顶点的颜色把其他点的颜色插值出来。这种想法在过去是可以的,比如Gouraud着色法就是这么干的。不过大家一定要记住,这种“插值”的算法只对类似于“位置”一类的东西有效果,比如3D坐标,3D向量都是可以进行插值的。但是对于“颜色”“距离”这种东西是效果极其差的,我举个例子。比如说距离,三角形的两条边大家都知道比垂线要长吧,但是如果使用这种插值算法进行长度插值的话就会得到垂线长度等于两边长度之和的平均这种一眼就能看出是错误的例子。再比如说颜色,光源如果正对着三角形,很明显三角形的中点是最亮的,但是根据这种插值算法肯定会得到三个顶点亮度的平均值,这就问题很严重了。所以说大家在着色的时候尽量都在光栅化之后进行着色运算,这样才能得到正确的结果。

OK,上面所说的就是所有的基础知识了,相信大家有了这些基础知识,再来看下面的代码实现就会更容易一些。下面我们具体的开始讲解GPU上如何实现上面我们说的这些算法。注意上面的算法大致分为三类,顶点层次的,光栅化的,以及光栅化之后的着色的。我们之前说了,光栅化的部分由硬件包办了,也就是说我们现在只需要实现两个部分就可以了,其一是顶点层次的,也就是3D级别的算法,我们称之为顶点着色器(vertex shader)。其二是光栅化之后的像素级别的,我们称之为像素着色器(pixel shader)或者OpenGL里面称之为片段着色器(fragment shader)。也就是说我们的GPU上的程序并不像CPU上的语言那样一路写到尾的,而是分开来的。这种程序的写法有点像topcoder的比赛一样,我们来设计函数就可以了,输入和输出都是固定的。而这里我们除了要设计函数以外,就是指定我们需要的输入输出的格式,也就是输入的顶点格式结构体长什么样子,以及我们希望光栅化的时候所保留的结构体数据长什么样子。当然有人就说了,卧槽,你这是逗我呢,我设计个GPU的程序还要束手束脚的,我就想一路走到黑,全部GPU程序由我控制怎么破?那很简单啊,如果你很擅长并行程序设计的话,你可以用通用计算(compute shader)去设计自己的所有流程,但是很明显,人家通过硬件封装的光栅化流程肯定比你自己写的任何软件算法都要快很多。而之所以把GPU程序拆成这么三段就是为了方便并行处理,能在不同的阶段最大化的利用GPU多核多线程的特点去更快的解决问题,如果把几千个线程一股脑的交给你其实你还真不一定能把程序设计的有多快,一个最简单的例子就是geometry shader与曲面细分,前者因为不好把细分过程并行化,最终还是得改成后者的硬件集成模式才能更好地进行细分曲面的工作。当然通用计算在一些图形算法中也是很重要的,比如HDR的pass1,但是那也只是借助其特性而已,并不算是代替传统的光栅渲染管线,这个我们后面讲到的时候会细说。那么废话不多说,下面来看今天我们的一个最简单的着色器:

float4x4         final_matrix;     //总变换struct Vertex_IN{float3pos : POSITION;     //顶点位置float3normal : NORMAL;       //顶点法向量float2tex     : TEXCOORD;      //顶点切向量};struct VertexOut{float4 position      : SV_POSITION;    //变换后的顶点坐标float3 color         : NORMAL;    //变换后的顶点坐标};VertexOut VS(Vertex_IN vin){VertexOut vout;vout.position = mul(float4(vin.pos, 1.0f), final_matrix);if (vin.normal.r < 0.0f && vin.normal.g < 0.0f && vin.normal.b < 0.0f){vin.normal.r = 1.0f;vin.normal.b = 1.0f;}if (vin.normal.r < 0.0f){vin.normal.r = 0.0f;}if (vin.normal.g < 0.0f){vin.normal.g = 0.0f;}if (vin.normal.b < 0.0f){vin.normal.b = 0.0f;}vout.color = vin.normal.xyz;return vout;}float4 PS(VertexOut pin) :SV_TARGET{return float4(pin.color,1.0f);}technique11 colorTech{pass P0{SetVertexShader(CompileShader(vs_5_0, VS()));SetGeometryShader(NULL);SetPixelShader(CompileShader(ps_5_0, PS()));}}
上述就是一个全套的着色器(fx类型的),fx是微软将自己的着色器封装的一种格式,这样大家就更容易切换和管理着色器了,而OpenGL是没有这种东西的,所以大家得自己管理自己的每一段着色代码。这个着色器呢很简单,输入的顶点结构体只有(位置,法向量,纹理坐标),我们指定的光栅化的时候保留的结构更是只有两个(位置,颜色)。这里为了尽量使得着色器简单,我们先不扯光照,直接把法向量当做颜色输出去。整个程序就非常简单,先把顶点进行变换,投影,然后光栅化之后把每个点的法向量当作这个点的颜色输出出去就行了。注意,无论你想保留多少变量在结构体里面都是可以的,但是在顶点着色器的输出里面一定要有一个SV_POSITION代表投影后的二维矢量,这个是光栅化必须要用的。然后在像素着色器里面最终的输出一定是这个像素的颜色。基本上这两个程序的用途就是这样,注意顶点着色器部分的函数是每个顶点都会跑一次的,然后像素着色器部分的函数是每个光栅化之后的三角形里面的每个像素点都会跑一次的,后者的工作量一般都比前者大一些。然后这里的语法是HLSL的语法,和c语言几乎是一毛一样的。大家也很容易应该就能看懂,注意这里的顶点输入格式一定要和之前创建的几何体的顶点格式一样,不然的话就很容易出问题。还有一点,大家看到那个final_matrix只是定义就被后面用了,可能会有一些疑惑,其实这个变量我们称之为外部变量,也就是说他不由GPU赋值,是由CPU赋值的,也就是说这个矩阵我们每一帧计算一次,然后把它赋值给GPU上的这个程序的。之所以这么做是因为大部分这种变量其实每一帧都在变化,当然对于大型的变量我们可不能这么做,只能通过资源绑定的方法进行访问,这个我们回头讲到再说。

那么上面我们讲完了GPU上的程序怎么设计,接下来我们就要说最后一步了,当我们在显存里面存储了一个几何体,并且怎么渲染几何体的算法也已经写好了之后。如何才能告诉计算机调用这个算法去绘制这个几何体呢?这就是directx要干的事情了。其实这个也很简单,首先我们要做的就是把那些GPU上的算法先编译了,因为毕竟是两种硬件上的语言,只能先编译好一个,然后交给directx。这个调directx编译的代码意义不大,大家看看就好了,反正对于每一个shader而言都是一样的而且和我们讲的图形学关系不大。注意vs2015是可以在编译阶段对fx一类的HLSL代码进行预编译的,编译成二进制代码之后,我们其实就是调用directx来得到一个shader指针就好了,就不需要在程序运行的时候再编译了。

HRESULT shader_basic::combile_shader(LPCWSTR filename){//创建shaderUINT flag_need(0);flag_need |= D3D10_SHADER_SKIP_OPTIMIZATION;#if defined(DEBUG) || defined(_DEBUG)flag_need |= D3D10_SHADER_DEBUG;#endif//两个ID3D10Blob用来存放编译好的shader及错误消息ID3D10Blob*shader(NULL);ID3D10Blob*errMsg(NULL);//编译effectstd::ifstream fin(filename, std::ios::binary);if (fin.fail()) {MessageBox(0, L"open shader file error", L"tip", MB_OK);return E_FAIL;}fin.seekg(0, std::ios_base::end);int size = (int)fin.tellg();fin.seekg(0, std::ios_base::beg);std::vector<char> compiledShader(size);fin.read(&compiledShader[0], size);fin.close();HRESULT hr = D3DX11CreateEffectFromMemory(&compiledShader[0], size,0,device_pancy,&fx_need);if(FAILED(hr)){MessageBox(NULL,L"CreateEffectFromMemory错误!",L"错误",MB_OK);return E_FAIL;}safe_release(shader);//创建输入顶点格式return S_OK;}

上述代码的最终的目的呢,就是把我们之前写的fx代码编译成一个ID3DX11Effect*的指针,就是这么简单的一段代码,有了这个指针之后,我们就可以根据这个指针来干两件事情,第一是通过这个指针给shader的外部变量赋值,赋值的方法很简单,先定义一个外部变量指针,然后把这个外部变量指针注册到这个ID3DX11Effect*里面,然后就可以随意的操控外部变量指针来复制了。下面展示如何进行赋值操作:

ID3DX11EffectMatrixVariable           *project_matrix_handle;      //全套几何变换句柄project_matrix_handle = fx_need->GetVariableByName("final_matrix")->AsMatrix();HRESULT shader_basic::set_matrix(ID3DX11EffectMatrixVariable *mat_handle, XMFLOAT4X4 *mat_need){XMMATRIX rec_mat = XMLoadFloat4x4(mat_need);HRESULT hr;hr = mat_handle->SetMatrix(reinterpret_cast<float*>(&rec_mat));return hr;}
其中fx_need就是我们所说的之前编译的ID3DX11Effect*类型的指针。接下来我们说这个指针的第二个用处,那就是可以调用这个指针来获取相应的渲染路径来渲染几何体,神马是渲染路径呢,这个是effect格式所独有的,也就是说指定究竟使用fx文件里面哪个vertex shader以及哪个pixel shader来渲染物体,路径的名字大家可以在之前的GPU代码上看到,就是technique11 colorTech那一行,colorTech就是我们所写的渲染路径,指定的vs和ps在那行代码的下面就可以看到。下面我们来看看怎么渲染物体:


void color_shader::set_inputpoint_desc(D3D11_INPUT_ELEMENT_DESC *member_point, UINT *num_member){//设置顶点声明D3D11_INPUT_ELEMENT_DESC rec[] ={//语义名    语义索引      数据格式          输入槽 起始地址     输入槽的格式 { "POSITION",0  ,DXGI_FORMAT_R32G32B32_FLOAT   ,0    ,0  ,D3D11_INPUT_PER_VERTEX_DATA  ,0 },{ "NORMAL"  ,0  ,DXGI_FORMAT_R32G32B32_FLOAT   ,0    ,12 ,D3D11_INPUT_PER_VERTEX_DATA  ,0 },{ "TEXCOORD",0  ,DXGI_FORMAT_R32G32_FLOAT      ,0    ,24 ,D3D11_INPUT_PER_VERTEX_DATA  ,0 }};*num_member = sizeof(rec) / sizeof(D3D11_INPUT_ELEMENT_DESC);for (UINT i = 0; i < *num_member; ++i){member_point[i] = rec[i];}}HRESULT shader_basic::get_technique(ID3DX11EffectTechnique** tech_need,LPCSTR tech_name){D3D11_INPUT_ELEMENT_DESC member_point[30];UINT num_member;set_inputpoint_desc(member_point,&num_member);*tech_need = fx_need->GetTechniqueByName(tech_name);D3DX11_PASS_DESC pass_shade;HRESULT hr2;hr2 = (*tech_need)->GetPassByIndex(0)->GetDesc(&pass_shade);HRESULT hr = device_pancy->CreateInputLayout(member_point,num_member,pass_shade.pIAInputSignature,pass_shade.IAInputSignatureSize,&input_need);if(FAILED(hr)){MessageBox(NULL, L"CreateInputLayout错误!", L"错误", MB_OK);return hr;}contex_pancy->IASetInputLayout(input_need);input_need->Release();input_need = NULL;return S_OK;}void Geometry<T>::show_mesh(){ID3DX11EffectTechnique teque_pancy;color_need->get_technique(&teque_pancy, "colorTech");UINT stride_need = sizeof(T);     //顶点结构的位宽UINT offset_need = 0;                       //顶点结构的首地址偏移//顶点缓存,索引缓存,绘图格式contex_pancy->IASetVertexBuffers(0, 1, &vertex_need, &stride_need, &offset_need);contex_pancy->IASetIndexBuffer(index_need, DXGI_FORMAT_R32_UINT, 0);contex_pancy->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);//选定绘制路径D3DX11_TECHNIQUE_DESC techDesc;teque_pancy->GetDesc(&techDesc);for (UINT i = 0; i < techDesc.Passes; ++i){teque_pancy->GetPassByIndex(i)->Apply(0, contex_pancy);contex_pancy->DrawIndexed(all_index, 0, 0);}}

上面的三个函数就是调用绘制的函数,而这三个函数就是最终绘制的时候每一帧需要运行的函数。第一个函数的作用就是指定顶点的类型,之前我们说过结构体大家可以自己定义,这里就是通过这个函数来告知GPU最终的结构体的格式长什么样子的。注意大家之前看GPU代码的时候肯定发现顶点格式结构体里面,定义方式和CPU的结构体有那么一丝区别,就是后面会有POSITION啊SV_POSITION啊这些修饰词,这就是用来标识变量作用的,这里也是依靠这些修饰词来把GPU上的格式和CPU上的结构体格式一一对应起来的。第二个函数就很明显了,根据我们需要的渲染路径的名字,从之前编译的着色器指针里面找到对应的着色用的technique指针。然后最后一步就是利用这个指针来告知directx要进行渲染了。也就是第三个函数所做的工作。这里的代码层层递进,大家应该很容易就能看懂。

如果大家所有的操作的完成无误的话,最终就会得到一个如下的旋转彩色立方体。这也就标志着大家这一节课所学的知识都完成了:


大家也许发现了这篇文章大部分的篇幅其实都在讲解一些原理的知识,而代码方面的介绍相比之下就显得不是很详细了,这个我要在这里声明一下,其实大家学到后面就会发现这些代码方面的知识对于新学习这方面知识的人来说,一开始可能会显得很难,但是随着大家的熟悉程度增加,这些知识其实都是很简单的,而一些原理的理解到那时会显得很重要,因为一点点的原理理解错误都有可能导致一些严重的bug。所以,大家在学习的时候也是一样的,我的准则是,大家宁可是看懂所有的原理之后把代码复制到自己计算机上跑出结果之后多改效果多看,也不要光是一步步的照着教程的代码按部就班的写下来而不知其所以然敲打。好了,下面是这节课的最终的代码地址,大家可以靠这个来更好的理解上述的知识:

源代码下载

2 0