第四章 Direct3D学习之模型渲染

各位女士们先生们!lolis and bad men!!停车坐爱枫林晚,从此君王不早朝!经过了N多天惨无人道的加班,三哥终于又活着和大家见面啦。

经过了之前的准备知识,今天我们来看一下真正的3D渲染。表明一下,进度显得有些快,这是因为,毕竟纯理论的东西是本书上都有。所以我只跟大家介绍一下如何理解这些理论,以及将这些枯燥的理论变成生动的游戏。很多教程上都是从顶点结构和渲染管线开始讲起,不可否认。那些东西确实是DIRECT3D的基础,但是经过了长期的自学。我很理解大家的心情,绝大部分的人在学习的初期都想看到真正能做出东西的程序。而这些教材上都是将枯燥而难懂理论讲了好几章,好多人在这个阶段就已经丧失了继续学下去的耐心。而跳过这些理论直接看后面的高级例子,漏掉的东西又实在太多。导致大部分初学者根本看不懂。

在这里,三哥将站在一个初学者的角度上给大家介绍最最必要的基础理论,然后以最简练的代码给大家展示一个3D程序是如何制作的。OK,为什么说在这时讲定点结构和渲染管线是个错误呢。因为一个没有接触过D3D编程或者是刚刚接触D3D编程的人。他们总会在学习之初先对这个技术有一个大致的想象。没错,他们认为模型是美工制作的,而程序是完成了模型的渲染和游戏的逻辑以及互动等。而顶点结构却是在讲怎么用程序来制作一个模型,再加以渲染,而且这个结构还十分复杂,这就给许多初学者留下了恐惧学习的心理阴影。这就是我为什么要先讲模型渲染的原因。等大家慢慢对D3D有个清晰的认识以后,再学习顶点就容易的多了,从而在学习更高级的特效渲染和程序优化。

言归正传,说了一大堆开场白。现在开始正式的内容,之前的三章讲了怎样在一个win32窗口上初始化一个D3D程序。上一章程序中写了一个渲染函数bool Display(float eventime);但是这个函数中没有渲染任何东西,这一章我们来渲染一个3D模型。

在开始渲染模型之前,先来认识几个必要的基本要素。

1.摄像机

2.灯光

3.世界坐标

用过3D建模工具的人,对这三个要素应该不会陌生。

首先,摄像机就是观察者,摄像机的位置就是观察者的位置。摄像机拍摄的图像就是我们在屏幕上看到的画面。然后摄像机有自己的坐标系,记住,是坐标系,一会在详细解释。

再则,灯光,一个虚拟世界,如果有了灯光我们就会看到明暗分明的3D物体,当然这要先设定灯光可用。如果关闭灯光可用,所有的物体都将没有立体感。特别说一下,如果你设置了灯光可用,但又没有添加灯光,那么你将看到一片漆黑,没有任何物体。当然这里简单的先介绍一下,关于灯光可以写半本书,以后我们一起详细研究。

最后,讲讲坐标的问题。D3D中有准确的说有两个坐标系,一个是世界坐标系,一个是视图坐标系。没错,摄像机就是在视图坐标系。其他的所有物体都在世界坐标系。需要说明,一切坐标和向量,在最终都会转换为矩阵为D3D所用,所以以后可能常用矩阵这个词来代替坐标。

这里有几个非常容易误导初学者的误区:

第一,根本就没有摄像机这个东西。所谓的摄像机只不过是我们为了方便理解比喻出来的,确切的说,只有一个视图矩阵来告诉Device,观察者的坐标是哪里,观察者的观察点在哪里,观察者的站立方向是哪个方向,还有观察者的观察范围是多大。例如下面代码就是在设置一个视图空间:

//设置摄像机(视图空间)
    D3DXVECTOR3 look_position(0.0f,0.0f,120.0f);
    D3DXVECTOR3 look_targetpoint(0.0f,0.0f,70.0f);
    D3DXVECTOR3 look_worldup(0.0f,1.0f,0.0f);
    D3DXMatrixLookAtLH(&lookAt,&look_position,&look_targetpoint,&look_worldup);
    Device->SetTransform(D3DTS_VIEW,&lookAt);

第二,视图坐标系的原点永远在摄像机上。比如我们设定了视图矩阵的初始位置是(0,0,0),当然初始值是针对世界坐标说的,如果我们把摄像机按向量(x,y,z)移动了一个位移,如果这时我们想要旋转摄像机,那应该按照那个轴来旋转呢。按照正常的理解应该是被移动之后的Y轴(x,1,z)。那么你错了,仍然还是(0,1,0)即Y轴。也就是说,视图坐标系的原点会随着视图矩阵的移动而移动。同样如果你旋转了视图矩阵,那么视图坐标轴将会被旋转。也就是说你可以把视图坐标系看作站立的人,他目视的方向是z,他头顶的方向是y,他左手的方向是x。无论这个人移动了,还是躺下了,他目视的方向仍然是x,他所在的位置仍然是原点。引用一些书上的图解来使读者更容易理解,假设世界空间是图中的坐标轴,那么三角形代替的范围就是视图空间所能看到的区域,可以看到,视图空间的原点和方向是随摄像机变化的:

image

第三,世界坐标系。物体所处的坐标系为世界坐标系。世界坐标系不随着物体的移动和旋转而变化,他是个恒定不变的存在。这似乎很好理解。但是D3D的代码方式使得很多人对世界坐标系的理解做不到上述的认识,为什么呢,因为

Device->SetTransform(D3DTS_WORLD,&worldMatrix);这句代码的意思是,将矩阵worldMatrix设置为世界矩阵(这里可以将矩阵简单的理解为坐标)。我先用伪代码来讲解一下:如果想要将物体A移动到(0,0,100)这个点,将物体B移动到(0,50,0)这个点。那么你需要这么做:

D3DXMatrixTranslation(&worldMatrix,0.0f,0.0f,100.0f);//设置一个位移矩阵worldMatrix,位移矢量(0,0,100)

Device->SetTransform(D3DTS_WORLD,&worldMatrix);//将该矩阵设置为世界矩阵

A.Draw();//渲染物体A

D3DXMatrixTranslation(&worldMatrix_1,0.0f,50.0f,0.0f);//设置另一个位移矩阵worldMatrix_1,位移矢量(0,50,0)

Device->SetTransform(D3DTS_WORLD,&worldMatrix_1);//将该矩阵设置为世界矩阵

B.Draw();//渲染物体A

大家会发现,按照代码来理解。并不是物体被移动了,恰恰是世界坐标系被移动了。而渲染物体A和B的过程完全没有位置信息。这样就给很多人以错觉,觉得是世界坐标总在变,而物体恒定不变。其实你可以这样理解,每个模型物体都是由点线面构成,而每个点在创建之初都会有自己的原始坐标,这个坐标就是在建模工具建模时的那个坐标。而这个坐标所参考的坐标系称为物体的本地坐标系。当然本地坐标系这个概念在D3D的代码中是体现不出来的。然而,模型其实就是一个一个点的集合,这些点又是以坐标的形式出现在模型文件中。渲染模型的过程其实就是按照文件中每一个坐标去绘制点,但是模型的文件是写死的,他的坐标是不会变化的。那么怎样才能让模型真正的移动呢,D3D在渲染时使用了矩阵变换。也就是按照矩阵对每个点做同样的计算,得出的新坐标再渲染。而这个矩阵就是寄存在我们设置的D3DTS_WORLD中,我们也就看到了物体被渲染在矩阵所指定的位置上。而D3D所参照的坐标是永远不会变的,只是它在渲染每个物体时的变换矩阵发生了变化。当然,矩阵不光能实现平移变换。旋转和缩放变换也都是通过矩阵来完成。这些矩阵为什么能实现不同的变换效果还请大家自己去好好学习线性数学。

      上述的知识是D3D中画图必不可少的,也是必须的弄懂基础知识。下面介绍一下被渲染的主角,网格模型。网格模型在D3D中以ID3DXMesh的形式出现。这个类有多个扩展的子类,我个人觉得对于它的研究应当深入,当然这不是我们今天的重点,这个类里包含模型的顶点数据,顶点的反射方式,材质信息,以及贴图坐标,当然,没有贴图的具体信息。贴图被存储在Textures类当中。通常由外部的图片来加载。mesh(网格模型的实例)可以自己创建,使用D3DXCreateMeshFVF函数,当然我觉得自己用代码来创建一个模型完全是没事闲的。另一种方式就是从模型文件来加载。外部模型文件有多种格式,这里我们使用官方支持的.X文件。有很多人说.X文件有多么不好,包括商业上基本没有人使用等,这个我没有评论的资格。但是我要说,商业上也基本没人直接用D3D去开发游戏,都是用游戏引擎来省时省力,但是我的目标是成为一个出色的游戏引擎制作专家。所以请大家记住,万变不离其中,再牛逼的引擎也是用D3D做出来的(相对于windows平台),再省事的模型文件也无非都是包含哪些信息,学习的时候不要追风,踏实的学会其中的道理你会发现之后的路一马平川。.x文件包括模型顶点,材质,贴图坐标,贴图的文件名,和动画等信息。可以通过3DMAX等著名的模型制作软件导出,下节课我们详细的讨论一下x文件的导出。现在我们来看看x文件的加载和使用。首先dx sdk中包含了一些官方的教程和例子,英语没有障碍的同学可以去看一下,很不错的,如果你能看懂的话。Microsoft DirectX SDK (November 2008)\Samples\Media路径下有SDK中使用到的一些x模型,我们先拿一个作为例子,打开Microsoft DirectX SDK (November 2008)\Samples\Media\Airplane我们会看见3个文件。一个x模型文件两个该模型的贴图文件。x模型的另一个好处就是材质贴图支持多种图片格式,而且包括带有透明信息的png格式。将这三个文件拷贝到与你工程的源代码同一个路径下。载入x文件的代码如下:

HRESULT hr;

//D3DXLoadMeshFromX参数

//从文件airplane 2.x中载入

//使用系统内存

//设备指针

//返回模型的邻接信息

//返回模型的材质数组

//返回D3DXEFFECTINSTANCE结构数组,我们现在不需要,使用0忽略此参数

//返回模型的材质数

//返回模型
if(FAILED(hr = D3DXLoadMeshFromX(“airplane 2.x”,D3DXMESH_SYSTEMMEM,Device,&adjBuffer,&mtrlBuffer,0,&numMtrls,&mesh))){
    MessageBox( NULL, "Could not find x", filename, MB_OK );
    return false;
}
if(mtrlBuffer!=0&&numMtrls!=0){

//提取材质数组
    D3DXMATERIAL* mtrls = (D3DXMATERIAL*)mtrlBuffer->GetBufferPointer();
//用于最后显示的材质

    Mtrls = new D3DMATERIAL9[numMtrls];

//用于最后显示的纹理贴图

    Textures = new LPDIRECT3DTEXTURE9[numMtrls];
    for(int i=0;i<(int)numMtrls;i++){

//使环境光与漫反射光相同
        mtrls[i].MatD3D.Ambient = mtrls[i].MatD3D.Diffuse;
        Mtrls[i] = mtrls[i].MatD3D;
//根据材质中的贴图文件名创建纹理贴图,并保存在Textures 中

        if(mtrls[i].pTextureFilename!=0){
            D3DXCreateTextureFromFile(Device,mtrls[i].pTextureFilename,&Textures[i]);
        }else{
            Textures[i] = 0;
        }
    }
}

 

在上面的代码中提到了很多还没有介绍的概念,如材质,纹理,贴图等。。不过我相信很多人这个都是懂的。不懂的朋友关注下一章。我将主要讲解模型文件的构成和制作与导出。

下面就可以开始渲染了:

if( Device ) // Only use Device methods if we have a valid device.
{

//用该物体的本地矩阵设置世界空间
    Device->SetTransform(D3DTS_WORLD,&worldMatrix);

//循环每个材质
    for(int i=0;i<(int)numMtrls;i++){

//设置材质
        Device->SetMaterial(&Mtrls[i]);

//设置纹理
        Device->SetTexture(0,Textures[i]);

//渲染该材质对应的模型子集
        mesh->DrawSubset(i);
    }
}

OK,让我们看一下最终的效果,如果没有看到最终结果,首先确认你的摄像机是否正对着模型,然后再确认灯光是否开启。

image

posted @ 2011-10-29 12:46  重甲土拨鼠  阅读(2642)  评论(0编辑  收藏  举报