欢迎阅读指正和转载,但请尊重原作者的工作,转载时请务必注明转载自:www.xionggf.com
 
深度探索DxFramework
 
第11章 3D primitives的剖析 1
 
经过上一章关于dxframework图元类C_Pritimive的分析,相信读者对于图元类的实现原理已经是了然于胸。从本质而言,图元无非就是一系列顶点和顶点索引(如果有的话)的集合。图元类把顶点及顶点索引的属性和对它们的相关操作做了封装。
既然明白了图元类的实现原理。相信读者应该是跃跃欲试,想尝试一下用图元类写一些程序了。在试用图元类前不妨精细研读一遍dxframework中使用图元类的示例程序。上一章只是从原理角度上分析了图元类,显得有点纸上谈兵。那么紧接下来的这一章,便是从实战角度出发,剖析最简单的应用图元类的示例程序分析。
 
11.1.示例demo中的各种图元
 
正如上一章的标题所示,图元是构造整个三维世界的基石。无论是简单如一两个三角形的演示程序,还是复杂如3D MMORPG那么广阔的世界场景。从本质而论这些都是由一个或者是多个图元所构建而成的。以dxframework的角度观之,所有的3D渲染归根到底都可以归结为对C_Primitive图元类的渲染操作。
 
图元有简单有复杂。但是复杂的图元都可以视为由简单的图元,通过一系列的修改操作而组成。有使用过像3DS MAX,Maya,TrueSpace等三维建模工具软件的读者应该很清楚。所有的复杂模型,都是通过对一些简单的基础的图元,比如球体,圆柱,正方体,锥体等等,进行各种各样的拉伸、压缩、切割、连接等操作而成。正因为如此,dxframework的第一个三维示例程序,就是展现了如何生成并使用一些规则几何体图元。
 
3D primitives demo提供了8种规则几何体图元,它们分别是线段、三角形、圆碟、正方形、锥体、圆柱、立方体、球。下面就先逐个分析这几个图元的原理,然后再深入剖析整个demo的实现。
 
11.1.1.线段(line)
 
从严格几何意义上来说,dxframework所提供的C_Line类应该称为线段(segment)类,而不是直线(straight line)类。因此本书把C_Line类称作线段类。线段可认为是一种图元,图元所共有的属性和方法线段也都有,因此,C_Line类是继承自C_Primitive类,如下:
 
class C_Line : public C_Primitive 
{
public:
    C_Line(); ///< Constructor
    virtual ~C_Line();  ///< Destructor
    virtual void Update(); 
};
 
上述的声明便是C_Line类的声明。按照C_Primitive的代码组织方法,初始化图元的顶点和顶点索引数据是在成员函数Update()中进行的。通过设定bool类型的成员变量updateMemory为false时,将在函数内创建顶点缓冲区和顶点索引缓冲区,并且写入顶点索引到顶点索引缓冲区(如果图元使用顶点索引的话)。设定bool类型的成员变量updateVertices为false时,将写入顶点数据进顶点缓冲区。C_Line类和其他的图元类遵循这个做法。
 
很显然,线段的顶点数据和绘制方法都是非常简单的。一个线段有两个端点,Direct3D所支持的D3D基本图元中,包括了线段列表(LINELIST)。所以通过在IDirect3DDevice8::DrawPrimitive函数中指定要绘制的基本D3D图元类型为D3DPT_LINELIST便可对线段进行绘制。代码如下:
 
void C_Line::Update() 
{
    if (updateMemory) {
        numVertices = 2; // 线段的顶点一直都为两个
        CreateVertexBuffer(numVertices);

        //一个线段就用一个渲染命令来进行渲染就可以了
        CreateCommandList(1);
        
        //按照线段列表的方式来进行渲染
        SetDrawPrimitiveCommand(0, D3DPT_LINELIST, 0, 1);
        updateMemory = false;
    }

    if (updateVertices) 
    {
        unsigned i;
        C_VertexManipulator vertexMemory;
        LockVertexBuffer(&vertexMemory);

        //设置线段两端点的位置坐标和法线向量
        vertexMemory[0].position = D3DXVECTOR3(0.0f, 0.0f, 0.5f*1.0f);
        vertexMemory[0].normal = D3DXVECTOR3(0.0f, 0.0f, 0.5f*1.0f);
        vertexMemory[1].position = D3DXVECTOR3(0.0f, 0.0f, -0.5f*1.0f);
        vertexMemory[1].normal = D3DXVECTOR3(0.0f, 0.0f, -0.5f*1.0f);

        //写纹理坐标
        for (i = 0;  i < (unsigned)numTexture; i++) 
        {
            //计算各纹理层的纹理映射坐标,成员变量textureOffset和
            //textureScale已经在C_Primitive类的构造函数中初始化 
            vertexMemory[0].texCoords[i] = D3DXVECTOR2( textureOffset[i].x, textureOffset[i].y);
            vertexMemory[1].texCoords[i] =
            D3DXVECTOR2( textureOffset[i].x+textureScale[i].x,textureOffset[i].y+textureScale[i].y);
        }

        //设置顶点的漫反射颜色,缺省不使用它而是使用图元的
        //材质颜色来进行来光照计算
        for (i = 0; i < (unsigned)numVertices; i++) 
        {
            vertexMemory[i].diffuseColor = 0xffffffff; 
        }

        //解除对顶点缓冲区的锁定,计算图元的包围盒
        UnlockVertexBuffer();
        updateVertices = false;
        p_BoundingBox->FindBoundingBox(this);
    }
}
 
上述的代码只是关于顶点的生成的,渲染部分的代码在哪里?因为C_Line类继承自C_Primitive类,C_Primitive类已经在成员函数Render3D()中实现了对顶点的渲染。而C_Line类不需要改变基类所实现的渲染行为。所以,C_Line类的渲染机制就是C_Primitive::Render3D()函数所实现的机制。
 
至此,C_Line类的代码就分析完毕了,是不是很简单?后续的几个图元的整体架构也是如此。都是通过指定精度(quality),在成员函数Update()中计算出图元的顶点数、三角形数、顶点索引数。然后往顶点缓冲区和索引缓冲区中写入数据。不同的是,各图元的几何特性都不同,所以要有不同的生成顶点方法。下面就接着分析三角形图元的实现。
 
11.1.2.三角形(triangle)
 
和C_Line类类似,作为图元的一种,三角形类C_Triangle也是继承自图元类C_Primitive。顾名思义,三角形拥有三个端点。所以C_Triangle类的顶点数为3。单个三角形使用了三角形列表(triangle list)的方式来进行渲染。值得注意的是三角形三个端点坐标的计算。每一个图元的坐标计算,都是基于其自身的模型空间坐标的。在模型空间坐标中,三角形的三个端点的Y值都为0,所以三角形其实实在坐标系的XZ平面上。三角形三个端点坐标的编号和分布如下图:
 
 
还有就是三角形三个端点的法线的计算。图元中的每个三角形都有一个与三角形所在平面垂直的表面法向量。该向量的方向由定义顶点的顺序,以及坐标系统是左手坐标系还是右手系坐标系来决定的。表面法向量从表面上指向正向面那一侧,如果把表面水平放置,即把它放到XZ平面上。则正向面朝上,背向面朝下,那么表面法向量为垂直于表面从下方指向上方。在Direct3D中只有面的正向是可视的。一个正向面是顶点按照顺时针顺序定义的面。因此Direct3D缺省默认的拣选方式是逆时针拣选,也就是说剔除掉顶点以逆时针顺序定义的背向面。
 
如果改变默认的拣选方式,也就是说把逆时针顺序定义的面视为正向面的话,那么这时候的正向面和背向面则刚好相反——正向面朝下,背向面朝上,表面法向量将垂直于表面从上方指向下方。同理三角形的三个端点的法向也是类似地做变化的。任何不是正向面的面都是背向面。由于Direct3D不总是渲染背向面,因此背向面要被剔除。如果想要渲染背向面的话,可以改变拣选模式。如下图:
 
 
了解了顶点位置和法线的计算方法之后,基本上C_Triangle类的实现原理也差不多了解了,代码如下:
 
class C_Triangle : public C_Primitive 
{
public:
    C_Triangle(); ///< Constructor
    virtual ~C_Triangle();  ///< Destructor
    virtual void Update();
};

C_Triangle::C_Triangle() 
{
    // Quality is ignored
}

C_Triangle::~C_Triangle()
{
}

void C_Triangle::Update() 
{
    if (updateMemory) 
    {
        //三角形有3个顶点,使用一个渲染命令
        numVertices = 3;
        CreateVertexBuffer(numVertices);
        CreateCommandList(1);
        SetDrawPrimitiveCommand(0, D3DPT_TRIANGLELIST, 0, 1);
        updateMemory = false;
    }

    if (updateVertices) 
    {
        int i;
        C_VertexManipulator vertexMemory;
        LockVertexBuffer(&vertexMemory);

        // Write vertex data
        vertexMemory[0].position = D3DXVECTOR3(0.0f, 0.0f, 0.5f*1.0f);    
        for (i = 0; i < numTexture; i++) 
        {
            vertexMemory[0].texCoords[i] = 
            D3DXVECTOR2(textureOffset[i].x + 
            textureScale[i].x * 0.5f, 
            textureOffset[i].y);
        }    
        
        vertexMemory[1].position =  D3DXVECTOR3(0.5f * 1.0f, 0.0f, -0.5f * 1.0f);

        for (i = 0; i < numTexture; i++) 
        {
            vertexMemory[1].texCoords[i] = D3DXVECTOR2(textureOffset[i].x + textureScale[i].x,
            textureOffset[i].y + textureScale[i].y);
        }
        
        vertexMemory[2].position = D3DXVECTOR3(-0.5f * 1.0f, 0.0f, -0.5f * 1.0f);

        for (i = 0; i < numTexture; i++) 
        {
            vertexMemory[2].texCoords[i] = D3DXVECTOR2(textureOffset[i].x, 
            textureOffset[i].y + textureScale[i].y);
        }
        
        //根据拣选模式的不同,图元的法线的朝向也应不同
        //图元的法线在顺时针拣选和在逆时针拣选的朝向互相相反
        switch (cullMode) 
        {
            //当拣选方式变为拣选顺时针的表面时,面上
            //的顶点朝向也就变为垂直XZ平面向下。
            case D3DCULL_CW:
                vertexMemory[0].normal = D3DXVECTOR3(0.0f, -1.0f, 0.0f);
                vertexMemory[1].normal =D3DXVECTOR3(0.0f, -1.0f, 0.0f);
                vertexMemory[2].normal = D3DXVECTOR3(0.0f, -1.0f, 0.0f);
                break;
            case D3DCULL_CCW:
                vertexMemory[0].normal = D3DXVECTOR3(0.0f, 1.0f, 0.0f);
                vertexMemory[1].normal = D3DXVECTOR3(0.0f, 1.0f, 0.0f);
                vertexMemory[2].normal = D3DXVECTOR3(0.0f, 1.0f, 0.0f);
                break;
        }

        //指定三角形的顶点漫反射颜色
        vertexMemory[0].diffuseColor = 0xffff0000;
        vertexMemory[1].diffuseColor = 0x0000ff00;
        vertexMemory[2].diffuseColor = 0x7f0000ff;
        UnlockVertexBuffer();
        updateVertices = false;

        //计算包围盒
        p_BoundingBox->FindBoundingBox(this);   
    }
}
 
三角形是组成各种复杂图元的基础,接下来的所有的图元都是基于三角形的。理解三角形图元的实现原理,将是深入研究下面复杂图元的必要条件。
 
11.1.3.正方形(square)
 
dxframework提供的C_Square类用来描述二维正方形。复杂的图形都可以使用一系列三角形来组织数据顶点数据。正方形类也不例外。在C_Square类中,把一个大的正方形分割成若干个小的正方形。每个小正方形由两个三角形组成,正方形图元分成若干个三角形来进行,渲染如下图:
 
 
在C_Square类的基类C_Primitive中,有一个保护型成员变量quality。quality表示图元的精度级别,在这里,quality的值表示的就是正方形图元可以按水平和垂直方向划分成几个相等的小正方形。如上图中quality等于5,表示这个正方形图元可以各沿水平方向和垂直方向划分成5个小正方形,则总的可以划分成5×5=25个小正方形。每个小正方形又由两个三角形组成。知道了图元的精度级别,就能推算出这个图元所需要的顶点数和顶点索引数。代码如下:
 

C_Square::C_Square()
{
    quality = 5; // 正方形使用水平方向5个,垂直方向5个,共25个小正方形组成
}

C_Square::~C_Square()
{
}

void C_Square::Update() 
{
    if (updateMemory) 
    {    
        // 正方形为25个,则顶点数为36个,三角形为50个,如上图
        numVertices = (quality + 1) * (quality + 1);
        numTriangles = quality * quality * 2;
        
        // 因为使用三角形列表的方式进行渲染,所以索引值的个数为三角形的个数的三倍
        numIndices = numTriangles * 3;
        CreateVertexBuffer(numVertices);
        CreateIndexBuffer(numIndices);

        unsigned short* p_DeviceIBMem;        
        LockIndexBuffer(&p_DeviceIBMem);

        // 写入三角形索引
        for (unsigned int row = 0; row < quality; row++) 
        {
            for (unsigned int col = 0; col < quality; col++) 
            {
                int baseIndex = 6 * (row * quality + col);
                // 每个小正方形左上角的顶点
                p_DeviceIBMem[baseIndex] = (row) *  (quality + 1) + col;
                
                // 每个小正方形右下角的顶点
                p_DeviceIBMem[baseIndex+1] = (row + 1) * (quality + 1) + col + 1;

                // 每个小正方形左下角的顶点
                p_DeviceIBMem[baseIndex+2] = (row + 1) * (quality + 1) + col;

                // 每个小正方形左上角的顶点
                p_DeviceIBMem[baseIndex+3] = (row) * (quality + 1) + col;

                // 每个小正方形右上角的顶点
                p_DeviceIBMem[baseIndex+4] = (row) *(quality + 1) + col + 1;

                // 每个小正方形右下角的顶点
                p_DeviceIBMem[baseIndex+5] = (row+1) * (quality + 1) + col + 1;
            }
        }
        UnlockIndexBuffer();
        CreateCommandList(1);
        SetDrawIndexedPrimitiveCommand(0, D3DPT_TRIANGLELIST,0, numVertices, 0, numTriangles);
        updateMemory = false;
    }

    if (updateVertices) 
    {
        int i;
        C_VertexManipulator vertexMemory;
        LockVertexBuffer(&vertexMemory);

        //计算顶点的坐标值
        for (unsigned int row = 0; row < quality + 1; row++) 
        {
            for (unsigned int col = 0; col < quality + 1; col++)
            {
                // 按照行优先的原则,计算每行从左到右的顶点坐标。,以正方形的中心点为本图元的模型空间的坐标原点
                int index = row * (quality + 1) + col; 
                vertexMemory[index].position = D3DXVECTOR3(-1.0f/2+col*1.0f/quality,
                0.0f,1.0f/2-row*1.0f/quality);

                switch (cullMode) 
                {
                case D3DCULL_CW:
                    vertexMemory[index].normal =D3DXVECTOR3(0.0f, -1.0f, 0.0f);
                    break;
                case D3DCULL_CCW:
                    vertexMemory[index].normal =D3DXVECTOR3(0.0f, 1.0f, 0.0f);
                    break;
                }

                //计算纹理坐标
                for (i = 0; i < numTexture; i++) 
                {
                    vertexMemory[index].texCoords[i] =D3DXVECTOR2(
                    textureOffset[i].x + textureScale[i].x * col / quality, 
                    textureOffset[i].y + textureScale[i].y * row / quality);
                }
            }
        }

        for (i = 0; i < numVertices; i++) 
        {
            vertexMemory[i].diffuseColor = 0xffffffff;
        }
        UnlockVertexBuffer();
        updateVertices = false;
        p_BoundingBox->FindBoundingBox(this);
    }
}

 
分析上面C_Square类的实现代码。计算C_Square类的顶点法线和计算C_Triangle类的顶点法线原理是一样的。都是根据拣选模式来确定。计算顶点坐标也类似,都是把正方形的中心点作为它的模型空间的原点。那么正方形的左上、左下、右上、右下四个端点的坐标则分别是(-0.5,0,0.5),(-0.5,0,-0.5),(0.5,0,0.5),(-0.5,0,-0.5)。其他的顶点则按顺序等分计算即可。书中代码有详细的注释。