欢迎阅读指正和转载,但请尊重原作者的工作,转载时请务必注明转载自:www.xionggf.com
 
深度探索DxFramework
 
第11章 3D primitives的剖析 2
 
11.1.4.圆碟(disk)
 
上面的三角形和正方形都可以视为由一个或多个三角形组成。那么碰上类似像圆这样的几何体应该如何模拟?答案也是使用三角形。从几何知识我们可以知道,要计算圆的面积,可以用计算圆的内接正多边形的面积来近似模拟。当圆的内接正多边形的边数越多时,它的面积就越逼近圆的面积。当内接正多边形的边数趋向无穷时,此时的正多边形的面积就等于正多边形的面积。同样地,这种利用极限求面积的思想,也可以应用在使用三角形模拟圆上。可以把圆划分成若干个三角形,当三角形划分得越细越多(这也是C_Primitive类中的成员变量quality的含义),则这一系列三角形就越逼近圆。dxframework中的圆碟类C_Disk就是采用这样的思想,它划分三角形的方法如下:
 
 
由上图可以看出,在给定半径的前提下,利用简单的正余弦便可以计算出组成三角形的顶点坐标值。关键之外在于如何计算夹角angle的值。C_Disk类采用的方法是,首先指定quality的值,然后把一个圆平均分为quality等分。编号为0的顶点到圆心连线和模型坐标空间的X轴的夹角为0;编号为1的顶点到圆心连线和模型坐标空间的X轴的夹角为360°/quality;编号为2的顶点到圆心连线和模型坐标空间的X轴的夹角为2×360°/quality ……依次类推,编号为N(0≤N≤qualtiy)的顶点到圆心连线和模型坐标空间的X轴的夹角为N×360°/quality。C_Disk类的顶点的法线计算方法C_Triangle类的一样。也是根据拣选方式来确定。分析了C_Disk顶点的生成方法之后,下面便是具体的实现代码。
 
class C_Disk : public C_Primitive
{
public:
    C_Disk();           ///< Constructor
    virtual ~C_Disk(); ///< Destructor
    virtual void Update(); 
};

C_Disk::C_Disk() 
{
    //圆碟的缺省精度为10个三角形组成,精度越大,则越接近圆
    quality = 10; // Set default quality
}

C_Disk::~C_Disk() 
{
}

void C_Disk::Update() 
{
    if (updateMemory) 
    {
        //使用三角形扇面的方式组织三角形,所以,所需要的顶点数就为三角形的数量+2
        numVertices = quality + 2; 
        CreateVertexBuffer(numVertices);
        CreateCommandList(1);
        SetDrawPrimitiveCommand(0, D3DPT_TRIANGLEFAN, 0, quality);
        updateMemory = false;
    }

    if (updateVertices) 
    {
        // Lock vertex and index buffers for write access
        unsigned i;
        C_VertexManipulator vertexMemory;
        LockVertexBuffer(&vertexMemory);

        // 写入顶点数据 把圆分为numVertices分
        for (int c = 0; c < (int)numVertices; c++) 
        {
            float angle = c * 2 * D3DX_PI / numVertices; 
            float x = 1.0f * 0.5f * sinf(angle);
            float z = 1.0f * 0.5f * cosf(angle);
            vertexMemory[c].position = D3DXVECTOR3(x, 0.0f, z);
            switch (cullMode) 
            {
            case D3DCULL_CW:
                vertexMemory[c].normal = D3DXVECTOR3(0.0f, -1.0f, 0.0f);
                break;
            case D3DCULL_CCW:
                vertexMemory[c].normal = D3DXVECTOR3(0.0f, 1.0f, 0.0f);
                break;
            }

            for (i = 0; i < (unsigned)numTexture; i++) 
            {
                vertexMemory[c].texCoords[i] = D3DXVECTOR2(
                textureOffset[i].x + textureScale[i].x * (x + 0.5f), 
                textureOffset[i].y + textureScale[i].y * (-z + 0.5f));
            }
        }

        for (i = 0; i < (unsigned)numVertices; i++) 
        {
            vertexMemory[i].diffuseColor = 0xffffffff;
        }

        UnlockVertexBuffer();
        updateVertices = false;
        p_BoundingBox->FindBoundingBox(this); 
    }
}
 
上面的代码是C_Disk的实现原理。圆的生成方法很重要,它是后续几个三维的图元的生成基础。理解C_Disk类对圆的实现方法是进一步剖析三维图元的前提。
 
11.1.5.圆锥(cone)
 
前面几个图元都是二维的图元。从这一节起将介绍三维的图元。圆锥是一个很常见的图元。一个闭合的圆锥可以视为由一个底(bottom)面和一个侧(side)面组合而成。底面和侧面又可以看作是由一系列的三角形组合而成。C_Cone类实现了圆锥这一种图元。
 
作为底面的圆,其实就和上节的C_Disk的原理一样的。也是根据类成员变量quality指定组成圆的三角形个数。根据指定的三角形个数quality,便能知道底面是一个正(quality+2)边形。当quality越大则底面的正多边形越逼近圆。知道组成底面的正多边形的边数很重要,因为圆锥的侧面能分割成几个三角形,就是由底面的多边形的边数所决定的。底面的多边形有N条边,则侧面就可以分割成N个三角形。如下图:
 
 
知道了圆锥的侧面和底面是如何分割成若干个三角形以及三角形的个数之后,就很容易地计算出三角形的顶点和顶点索引了。作为底面的那些顶点很容易计算,其计算方法和C_Disk类中计算顶点的方法是一样的。现在要考虑的是组成侧面的三角形的顶点。
 
dxframework使用三角形扇面的方式来组织侧面三角形。侧面三角形共用锥顶顶点。因为每个三角形都处在不同的平面上,可以把这些三角形投影到它的模型空间坐标的XZ平面上,从而能求得顶点的X坐标和Z坐标。至于Y坐标,很明显,除了共用锥顶的那个顶点之外,其他的顶点的Y坐标是完全一致的。顶点的俯视投影图如下:
 
 
根据示意图,便能计算出侧面和底面的顶点。代码如下:
 
class C_Cone : public C_Primitive 
{
public:
    C_Cone();            
    virtual ~C_Cone();  
    virtual void Update();
private:
    unsigned int numVerticesSides;    
    unsigned int numTrianglesSides;    
    unsigned int numVerticesBottom;    
};

C_Cone::C_Cone() 
{
    quality = 8;  //锥体的精度级别,其实也就是锥底的三角形个数。
                  //当精度指定为n时,底面就是一个正n+2边形。
                  //锥底由一系列的三角形扇面组成。
}

C_Cone::~C_Cone() 
{
}

void C_Cone::Update() 
{
    if (updateMemory) 
    {
        //由上面的图可以看出,侧面上的顶点数为精度级数加4
        //锥体的侧面的三角形个数为精度级数的值加2
        //锥体的底面的顶点数为精度数加2
        numVerticesSides = quality + 4;
        numTrianglesSides = quality + 2;
        numVerticesBottom = quality + 2; 
        numVertices = numVerticesSides + numVerticesBottom;
 
        CreateVertexBuffer(numVertices);
        CreateCommandList(2);
        SetDrawPrimitiveCommand( 0, D3DPT_TRIANGLEFAN, 0, numTrianglesSides);
        SetDrawPrimitiveCommand( 1, D3DPT_TRIANGLEFAN,numVerticesSides, quality);
        updateMemory = false;
    }

    if (updateVertices) 
    {
        C_VertexManipulator vertexMemory;
        LockVertexBuffer(&vertexMemory);
        vertexMemory[0].position = D3DXVECTOR3(0.0f, 0.5f*1.0f, 0.0f);

        if (cullMode == D3DCULL_CW) 
        {
            vertexMemory[0].normal = D3DXVECTOR3(0.0f, -1.0f, 0.0f);
        } 
        else 
        {
            vertexMemory[0].normal = D3DXVECTOR3(0.0f, 1.0f, 0.0f);
        }

        unsigned i;

        for (i = 0; i < (unsigned)numTexture; i++) 
        {
            vertexMemory[0].texCoords[i] =D3DXVECTOR2(
            textureOffset[i].x + textureScale[i].x*0.5f, 
            textureOffset[i].y + textureScale[i].y*0.5f);
        }

 
计算完顶点索引之后,接下来就是计算顶点的坐标了,方法是先把侧面的坐标先投影到XZ平面上来计算顶点的x坐标和z坐标。如下图
 
 
实现的代码如下:
 
        //接上段代码
        for (unsigned int c = 1; c < numVerticesSides; c++) 
        {
            //根据圆锥侧面的三角形个数,算出这些三角形在XZ平面上
            //的投影,每个两个锥体的棱的投影夹角,如上图的angle
            //所占的角度的大小。然后可以算出顶点的坐标
            float angle = (c-1) * 2 * D3DX_PI / numTrianglesSides; 
            float x =  0.5f * 1.0f * sinf(angle);
            float y = -0.5f * 1.0f;
            float z =  0.5f * 1.0f * cosf(angle);    
            vertexMemory[c].position = D3DXVECTOR3(x, y, z);

            //这些顶点的法线和原点到顶点的法线连线的方向相同
            switch (cullMode) 
            {
            case D3DCULL_CW:
                vertexMemory[c].normal = D3DXVECTOR3(x, y, z);
                break;
            case D3DCULL_CCW:
                vertexMemory[c].normal = D3DXVECTOR3(x, -y, z);
                break;
            }

            D3DXVec3Normalize(&vertexMemory[c].normal,&vertexMemory[c].normal);

            for (i = 0; i < (unsigned)numTexture; i++) 
            {
                vertexMemory[c].texCoords[i] = D3DXVECTOR2(
                textureOffset[i].x + textureScale[i].x * (x + 0.5f), 
                textureOffset[i].y + textureScale[i].y * (-z + 0.5f));
            }
        }

        //遍历底面的顶点,原理和C_Disk类的计算方法是类似的
        for (c = 0; c < numVerticesBottom; c++) 
        {
            unsigned int index = numVerticesSides + c;
            float angle = c * 2 * D3DX_PI / numVerticesBottom;
            float x = -0.5f * 1.0f * sinf(angle);
            float y = -0.5f * 1.0f;
            float z = 0.5f * 1.0f * cosf(angle);    

            vertexMemory[index].position = D3DXVECTOR3(x, y, z);

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

            for (i = 0; i < (unsigned)numTexture; i++)
            {
                vertexMemory[index].texCoords[i] = D3DXVECTOR2(
                textureOffset[i].x+ textureScale[i].x * (x + 0.5f), 
                textureOffset[i].y + textureScale[i].y * (-z + 0.5f));
            }
        }

        for (i = 0; i < (unsigned)numVertices; i++) 
        {
            vertexMemory[i].diffuseColor = 0xffffffff;
        }

        UnlockVertexBuffer();
        updateVertices = false;
        p_BoundingBox->FindBoundingBox(this);
    }
}
 
至此圆锥的生成算法就分析完了,接下来分析圆柱的生成算法。
 
11.1.6.圆柱(cylinder)
 
dxframework使用类圆柱由一个侧面和两个底面组成。圆柱和圆锥相似的地方就是都用圆作底面。圆柱是用两个相等的圆做两底面,圆锥是一个。圆柱和圆锥的底面生成算法是一样的。所以两者区别就在于侧面的生成算法。在dxframework中正如把圆锥的侧面展开将是三角形扇面一样,把圆柱的侧面展开,将是一个三角形条带,如下图:
 
 
从上图可以看出来,侧面三角形的个数将取决于底面多边形的边数,而底面多边形的数量呢?和圆碟,圆锥底面一样。用来模拟逼近圆的多边形的边数是取决于类C_Primitive的成员变量quality。在C_Cylinder类中它表示组成底面三角形的数量。知道了圆柱的底面和侧面的划分三角形的方法,顶点和顶点索引的计算也就是水到渠成了。代码如下:
 
class C_Cylinder : public C_Primitive 
{
public:
    C_Cylinder(); ///< Constructor
    virtual ~C_Cylinder(); ///< Destructor
    virtual void Update();
private:
    unsigned int segments; //圆柱的侧面四边形的数量,每两个三角形一个组成四边形,如上图
    unsigned int numVerticesSides;  //圆柱侧面的顶点数
    unsigned int numTrianglesSides;//圆柱侧面的三角形数
    unsigned int numVerticesBottom;//圆柱底面的三角形数
};

C_Cylinder::C_Cylinder() 
{
     quality = 8; //底面的三角形数
}

C_Cylinder::~C_Cylinder() 
{
}
 
圆柱由一个侧面和两个底面组成,所以圆柱使用了三个渲染命令分别负责对这三个面的渲染。其中侧面是使用三角形条带的方式,底面使用了三角形扇面的方式,代码如下:
 
//接上面的代码,cylinder.cpp
void C_Cylinder::Update() 
{
    if (updateMemory) 
    {
        segments = quality + 2;

        //三角形条带的顶点个数 = 三角形个数 + 2
        //三角形个数 = 2 * segments,所以顶点个数如下
         numVerticesSides = 2 * (segments + 1);    

        //每个侧面有两个三角形
        numTrianglesSides = segments * 2;
        // 顶点数就是侧面数,和cone一样,quality为n的话,底面就是正n+2边形
        numVerticesBottom = segments;
        numVertices = numVerticesSides + 2 * numVerticesBottom; 
        CreateVertexBuffer(numVertices);
        CreateCommandList(3);
        SetDrawPrimitiveCommand( 0, D3DPT_TRIANGLESTRIP, 0, numTrianglesSides);    
        SetDrawPrimitiveCommand(1, D3DPT_TRIANGLEFAN,numVerticesSides, quality);
        SetDrawPrimitiveCommand(2,D3DPT_TRIANGLEFAN, numVerticesSides + numVerticesBottom, quality); 
        updateMemory = false;
    }
 
计算侧面的顶点坐标的思想和方法与计算圆锥的相同,都是把底面的顶点投影到模型坐标空间的XZ平面上来计算顶点的x,z坐标值。两个底面的Y值都是一个定值,并且互为相反数。实现代码如下:
 
    //接上面的代码,cylinder.cpp 函数void C_Cylineder::Update()
    if (updateVertices) 
    {
        unsigned i;
        C_VertexManipulator vertexMemory;
        LockVertexBuffer(&vertexMemory);

        //计算侧面的顶点坐标,方法是把底面(顶面)投影到XZ平面。
        //上来计算,其原理和圆锥的是相同,要注意的是顶点的排列
        //方式而且在同一垂直线上的底顶面的两个坐标的X,Z坐标值
        //是相同的,只是Y值互为相反数。
        for (unsigned int c = 0; c <= segments; c++) 
        {
            unsigned int topSidePnt = c * 2;        
            unsigned int botSidePnt = topSidePnt + 1; 
            float angle = c * 2 * D3DX_PI / segments;
            float x = 0.5f * 1.0f * sinf(angle);
            float y = 0.5f * 1.0f;
            float z = 0.5f * 1.0f * cosf(angle);
            vertexMemory[topSidePnt].position = D3DXVECTOR3(x, y, z);
            vertexMemory[botSidePnt].position = D3DXVECTOR3(x, -y, z);    

            //侧面顶点的法线向量是和其模型坐标系的XZ平面是平行的。
            //其方向就是模型坐标系原点到顶点在XZ平面的投影点的连线方向
            switch (cullMode) 
            {
            case D3DCULL_CW:
                vertexMemory[topSidePnt].normal = D3DXVECTOR3(-x, 0, -z);
                break;
            case D3DCULL_CCW:
                vertexMemory[topSidePnt].normal = D3DXVECTOR3(x, 0, z);
                break;
            }
            D3DXVec3Normalize(&vertexMemory[topSidePnt].normal, &vertexMemory[topSidePnt].normal);
            vertexMemory[botSidePnt].normal =vertexMemory[topSidePnt].normal;
            for (i = 0 ; i < (unsigned)numTexture; i++) 
            {
                vertexMemory[topSidePnt].texCoords[i] = D3DXVECTOR2(
                textureOffset[i].x + textureScale[i].x * c / (segments - 1), 
                textureOffset[i].y);
                vertexMemory[botSidePnt].texCoords[i] = D3DXVECTOR2(
                textureOffset[i].x + textureScale[i].x * c / (segments - 1), 
                textureOffset[i].y + textureScale[i].y);
            }
        }

        for (c = 0; c < numVerticesBottom; c++) 
        {
            unsigned int topIdx = numVerticesSides + c;
            unsigned int botIdx = topIdx + numVerticesBottom;  
            float angle = c * 2 * D3DX_PI / numVerticesBottom; 
            float x = 0.5f * 1.0f * sinf(angle);
            float y = 0.5f * 1.0f;
            float z = 0.5f * 1.0f * cosf(angle);
            vertexMemory[topIdx].position = D3DXVECTOR3(x, y, z);
            vertexMemory[botIdx].position = D3DXVECTOR3(-x, -y, z);

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

            // 写入顶点纹理坐标
            for (i = 0; i < (unsigned)numTexture; i++) 
            {
                vertexMemory[topIdx].texCoords[i] =D3DXVECTOR2(
                textureOffset[i].x + textureScale[i].x * ( x + 0.5f), 
                textureOffset[i].y + textureScale[i].y * ( -z + 0.5f));
                vertexMemory[botIdx].texCoords[i] = vertexMemory[topIdx].texCoords[i];
            }
        }

        for (i = 0; i < (unsigned)numVertices; i++) 
        {
            vertexMemory[i].diffuseColor = 0xffffffff;
        }

        UnlockVertexBuffer();
        updateVertices = false;
        p_BoundingBox->FindBoundingBox(this);
    }
}
 
圆柱的生成算法也介绍完了,接下来介绍最后两个三维图元,球和立方体。