欢迎阅读指正和转载,但请尊重原作者的工作,转载时请务必注明转载自:www.xionggf.com
 
深度探索DxFramework
 
第11章 3D primitives的剖析 4
 
11.2 Demo分析
 
3D primitives demo所用到的八个图元已经分析完毕了,现在就来看一下demo的实现。3D primitives demo使用了八个图元。八个图元排成一圈,其中有一个图元正对着镜头。通过按左右箭头键,可以轮回切换当前正对着镜头的图元。每个图元还可以以一定的规律按它自身的模型空间坐标轴旋转,以便让用户能从多个角度观察它的外观。demo的运行时截图如下:
 
 
11.2.1.Demo的整体架构
 
上面的章节提过,每一个使用dxframework开发的游戏都将被视为一个game state。3D primitive demo也不例外,所以主游戏程序类C_PrimitiveDemo也继承自C_GameState类。
 
demo使用了八个图元来进行演示,那么必定就要有八个图元类的实例对象。C_PrimitiveDemo类使用了这八个图元类实例指针作为它的类成员变量。作为游戏实例类的C_PrimitiveDemo类则负责这些图元对象实例的产生和销毁。
 
单单有图元对象还不足以完成整个demo。想要观察这个世界,至少需要一个摄像机去摄取图像,需要一个视口去显示图像吧?至少需要一束灯光去照亮这个世界吧。至少需要一些文字去展现一些提示信息吧?这些3D primitives demo都考虑到了。负责管理摄像机和视口的C_ViewPort类;负责灯光的C_Light类;负责字体显示的C_Font类。都有实例对象作为C_PrimitiveDemo类成员变量。代码如下:
 
class C_PrimitiveDemo : public C_GameState
{
...
private:
    C_Sprite*        p_BgSprite;  //demo背景图的sprite
    C_Line*       p_Line;      //线段图元的实例指针
    C_Triangle*    p_Triangle; //三角形图元的实例指针
    C_Square*      p_Square;      //正方形图元的实例指针
    C_Disk*          p_Disk;      //圆碟图元的实例指针
    C_Cube*         p_Cube;      //立方体图元的实例指针
    C_Cone*          p_Cone;      //圆锥体图元的实例指针
    C_Cylinder*     p_Cylinder; //圆柱体图元的实例指针
    C_Sphere*       p_Sphere;       //球体图元的实例指针
    C_Font*           p_TextFont;    //字体实例指针
    C_Viewport*    p_Viewport;    //摄像机和视口类
    C_Light*          p_Light;     
... 
};
 
C_PrimitiveDemo类继承自C_GameState类。那么三个重要的虚拟成员函数:
 
public : void C_GameState::Update();
public: void C_GameState::Render3D();
public: void C_GameState::Input();
 

C_PrimitiveDemo类必须继承并实现之,从而实现本demo特定的图形渲染;外设输入响应和状态更新。在介绍完整体架构之后,接下来就是对具体实现细节的分析。具体实现细节将分几节讲述。

 
11.2.2.各图元的初始化
 
上面提到了,各个组成游戏的类实例对象都随着游戏实例的初始化而初始化,那么在C_PrimitiveDemo类的构造函数中:
 
C_PrimitiveDemo::C_PrimitiveDemo() 
{
    // 创建作为demo的场景背景的sprite
    p_BgSprite = new C_Sprite(_T("background_turbine_primitive.png"));

    //指定了所有图元材质的镜面反射值
    D3DCOLORVALUE rgbaSpecularReflect = {0.5, 0.5, 0.5f, 1.0f};

    //创建线段图元
    p_Line = new C_Line();

    //创建三角形图元对象
    p_Triangle = new C_Triangle();

    //设置纹理,使用单层纹理,第0层
    p_Triangle->SetTexture(0, _T("3d/dirt.png"));
    p_Triangle->SetCullMode(D3DCULL_NONE);
    //设置图元材质的镜面反射值
    p_Triangle->SetSpecularHighlight(rgbaSpecularReflect);
    //设置镜面反射系数
    p_Triangle->SetSpecularPower(20.0f);

    //使用顶点的漫反射颜色来执行漫反射光照计算
    p_Triangle->SetVertexDiffuseColorEnable(true);
    //使得图元产生半透明效果
    p_Triangle->Help_MakePrimitiveTransparent();
    // the square
    p_Square = new C_Square();
    p_Square->SetTexture(0, _T("3d/woodfloor.png"));
    p_Square->SetCullMode(D3DCULL_NONE);
    p_Square->SetSpecularHighlight(rgbaSpecularReflect);
    p_Square->SetSpecularPower(20.0f);

    p_Disk = new C_Disk();
    p_Disk->SetTexture(0, _T("3d/swirlmetal.png"));
    p_Disk->SetTextureScalingXY(0, D3DXVECTOR2(2.0f, 2.0f));
    p_Disk->SetQuality(25);
    p_Disk->SetCullMode(D3DCULL_NONE);
    p_Disk->SetSpecularHighlight(rgbaSpecularReflect);
    p_Disk->SetSpecularPower(20.0f);

    // the cube
    p_Cube = new C_Cube();
    p_Cube->SetTexture(0, _T("3d/water.png"));
    p_Cube->SetSpecularHighlight(rgbaSpecularReflect);
    p_Cube->SetSpecularPower(20.0f);

    // the cone
    p_Cone = new C_Cone();
    p_Cone->SetTexture(0, _T("3d/road.png"));
    p_Cone->SetTextureScalingXY(0, D3DXVECTOR2(2.0f, 2.0f));
    p_Cone->SetQuality(25);
    p_Cone->SetSpecularHighlight(rgbaSpecularReflect);
    p_Cone->SetSpecularPower(20.0f);

    // the cylinder
    p_Cylinder = new C_Cylinder();
    p_Cylinder->SetTexture(0, _T("3d/brownstucco.png"));
    p_Cylinder->SetQuality(25);
    p_Cylinder->SetSpecularHighlight(rgbaSpecularReflect);
    p_Cylinder->SetSpecularPower(20.0f);

    // the sphere
    p_Sphere = new C_Sphere();
    p_Sphere->SetTexture(0, _T("3d/sky.png"));
    p_Sphere->SetQuality(25);
    p_Sphere->SetSpecularHighlight(rgbaSpecularReflect);
    p_Sphere->SetSpecularPower(20.0f);
    p_Sphere->Help_MakePrimitiveTransparent();
    p_Sphere->Help_SetPrimitiveAlpha(0.8f);
    /* 上面的代码便是生成图元并初始化的代码,注意这些图元使用的纹理数,以及各种光照计算的参数。
    除了图元之外,还要生成和初始化诸如摄像机和视口类的对象。设置如下:*/
    p_TextFont = new C_Font(_T("Arial"), 14);
    distance = -10.0f;

    p_Viewport = new C_Viewport();

    //设置摄像机透射投影的属性,参数1为视野角的大小,参数2为视截体高
    //宽比,参数3为近截平面的Z值,参数4为远截平面的Z值
    p_Viewport->SetPerspective(D3DX_PI/4, 1.0f, 0.01f, 50.0f);

    //设置光照的环境光,漫反射光,镜面反射光
    D3DCOLORVALUE ambient = {0.5f, 0.5f, 0.5f, 1.0f};
    D3DCOLORVALUE diffuse = {1.0f, 1.0f, 1.0f, 1.0f};
    D3DCOLORVALUE specular = {1.0f, 1.0f, 1.0f, 1.0f};
    //设置灯光的类型为点光源
    p_Light = new C_Light(D3DLIGHT_POINT);
    p_Light->SetAmbient(ambient);
    p_Light->SetDiffuse(diffuse);
    p_Light->SetSpecular(specular);
    p_Light->SetPosition(D3DXVECTOR3(0.25f, 0.25f, -7.5f));
    p_Light->SetRange(15.0f);
    p_Light->SetAttenuation1(0.5f);
    p_Light->Update();
    p_Light->Enable();

    // 移动的方向
    movingDir = 1;

    //渲染
    gp_View->SetClear(true);

    theta = 0;  //图元所围成的“环”旋转的角度
    toMove = 0;

    //显示当前正对着镜头的图元
    UpdateShapetext();
}
 
构造出了图元的实例对象,还需要确定它们的最初位置坐标才行。从demo的运行时的截图可以看出,8个图元是呈一个环状排列的。图元的最初位置示意图如下图:
 
 
从上面的示意图中可以看出图元的最初放置位置。当由图元组成的“ring”(后面就称为“转盘”)的转动角发生变化的时候,组成这个“ring”的各个图元的位置也随之转动,转动的大小和距离则由theta的大小所决定。角度theta就表示转盘相对于初始位置所转过的角度,这个角度是以世界空间的Z坐标轴正半轴作为起始边的。C_Primitive类成员变量theta就表示这一角度。在什么情况下theta会发生变化?答案就是当有外设输入的时候,准确地说是按下左右箭头键转动转盘的时候。响应按下左右箭头键的时候theta将会发生变化。接下来便介绍对外设输入的响应。
 
11.2.3.对外设输入的响应
 
dxframework给每个game state实例都提供了响应外设输入的接口函数。这是一个虚函数,所以具体的游戏实例类按照自己的对外设输入的响应逻辑,将这函数实现便可。代码如下:
 
void C_PrimitiveDemo::Input()
{

    //按ESC键退回上一级主菜单
    if (gp_Controller->GetKeyState(DIK_ESCAPE) ==BUTTON_PRESSED) 
    {
        gp_Model->ChangeState(MENU);
    }

    //按左箭头键,图元逆时针旋转
    if (gp_Controller->GetKeyState(DIK_LEFT) == BUTTON_DOWN)
    {
        if (movingDir == 1) 
        {
            movingDir = 0;
            toMove = 45;
        }
    }

    //按右箭头键,图元顺时针旋转
    if (gp_Controller->GetKeyState(DIK_RIGHT) == BUTTON_DOWN)
     {
        if (movingDir == 1) 
        {
            movingDir = 2;
            toMove = 45;
        }
    }

    //按上箭头键,把镜头向图元拉近
    if (gp_Controller->GetKeyState(DIK_UP) == BUTTON_DOWN) 
    {
        distance += (float)(gp_Timer->GetElapsed() *ZOOM_SPEED);
    }

    //按下箭头键,把镜头向图元拉远
    if (gp_Controller->GetKeyState(DIK_DOWN) == BUTTON_DOWN) 
    {
        distance -= (float)(gp_Timer->GetElapsed() *ZOOM_SPEED);
    }
}
 
具体分析一下上面的响应按下左、右、上、下箭头的代码。已经知道按下左右箭头是转动转盘,上、下箭头把镜头向图元拉近、拉远。如当按下左箭头的时候,如果moveDir的值为1的话,表示当前转盘处于停止状态,可以让它向左转动,即把moveDir值设置为0,而如果按下左箭头头键的时候moveDir的值不为1,表示当前转盘还处于正在转动的状态,不能改变转动方向。toMove则表示按下了左箭头键后,希望转盘旋转的角度的大小。同样,按下右箭头键也是一样道理的。至于按下上下箭头,无非就是改变了distance成员变量,因为distance的值将作为摄像机自身在世界空间中的位置坐标使得它摄像机自身的位置坐标能发生变化,从而达到拉近拉远镜头的效果。通过对外设输入的响应,改变了转盘的转动角和摄像机的位置坐标,那么接下来在渲染之前,就要先更新图元的各种属性状态。
 
11.2.4.各游戏对象的状态更新
 
dxframework给每个game state实例都提供了更新各游戏对象的接口函数。这是一个虚函数,所以具体的游戏实例类按照自己的对各游戏对象的更新的逻辑,将这函数实现便可。在分析void C_PrimitiveDemo::Update()函数之前,先看看两个C_PrimitiveDemo函数:
 
inline void C_PrimitiveDemo::IncreaseAngle() 
{
    //把转盘向右转动,每一帧转动的度数,应为上一帧到本帧流逝的时间
    //乘以移动速率MOVE_SPEED。
    theta += gp_Timer->GetElapsed() * MOVE_SPEED;
    toMove -= gp_Timer->GetElapsed() * MOVE_SPEED;
    
    if (toMove < 0) 
    {
        theta += toMove;
    }
}

inline void C_PrimitiveDemo::DecreaseAngle() 
{
    //把转盘向左转动
    theta -= gp_Timer->GetElapsed() * MOVE_SPEED;
    toMove -= gp_Timer->GetElapsed() * MOVE_SPEED;

    if (toMove < 0) 
    {
        theta -= toMove;
    }
}
 
当转盘处于转动状态,也即moveDir等于0或者等于2的时候,每一次对void C_PrimitiveDemo::Update()函数的调用,都要增大或者是减小当前转盘的转动角。在改变当前转盘的转动角的同时,IncreaseAngle()函数和DecreaseAngle()函数还负责改变类成员变量toMove的值。toMove变量表示转盘当前的这次转动还剩下几度就应该停下来。theta变量每增大或者是减小一度,toMove变量则应减小一度。当toMove不断减小,减小到小于0的时候,就表示“转过头”了,就要调整过来。即用theta变量加上或者减去当前的toMove变量值。弄明白IncreaseAngle()函数和DecreaseAngle()函数后,便可以进一步分析void C_PrimitiveDemo::Update()函数了。代码如下:
 
void C_PrimitiveDemo::Update()
{
    //根据当前的移动方向,每帧更新当前转盘的转动角theta
    if (movingDir == 0) //向左转动
    {    
        DecreaseAngle(); //减小转动角
    } 
    else if (movingDir == 2)  //向右转动
    {    
        IncreaseAngle(); //增大转动角
    }

    //toMove变量小于0了,表示本次的转动结束
    if (toMove < 0) 
    {                 
        toMove = 0;    //要把toMove置换,放置下一帧还被调用这段代码
        movingDir = 1;//把移动方向标志设置为1,即停止

        if (theta < 0) //保持theta的值为正数
        {
            theta += 360;
        }
        
        theta = fmod(theta,360);//求余数,即当前的转盘转动角是多少
        
        //根据当前的转动角显示当前处于镜头前方的图元名字
        UpdateShapetext();
    }

    //调整镜头的位置,更新
    p_Viewport->SetCamera( D3DXVECTOR3(0, 0, distance),D3DXVECTOR3(0, 0, 0));
    p_Viewport->Update();

    //更新所有图形的状态
    p_Cube->Update();
    p_Disk->Update();
    p_Square->Update();
    p_Triangle->Update();
    p_Sphere->Update();
    p_Line->Update();
    p_Cylinder->Update();
    p_Cone->Update();
}
 
在函数中调用了成员UpdateShapetext(),用来显示当前正对镜头的图元名字。这个函数的代码如下:
 
void C_PrimitiveDemo::UpdateShapetext() 
{
    switch ((int)round(theta)) 
    {
    case 0:
    case 360:
        shape = _T("Sphere");
        break;
    case 45:
        shape = _T("Square");
        break;
    case 90:
        shape = _T("Line");
        break;
    case 135:
        shape = _T("Disk");
        break;
    case 180:
        shape = _T("Cube");
        break;
    case 225:
        shape = _T("Cylinder");
        break;
    case 270:
        shape = _T("Triangle");
        break;
    case 315:
        shape = _T("Cone");
        break;
    default:
        shape = toTString(theta);
    }
}
 
很明显,这个函数的意思是,根据转盘转动的角度判断当前正对着镜头的图元是哪个。从上面的图元初始位置摆放图中就能一一找到转动角度大小和当前正对着镜头的图元的映射关系。
 
11.2.5.渲染游戏对象
 
图元以及其他的游戏对象的状态更新之后,最后的任务就是把它们渲染出来。与前面几个二维示例程序相比,三维的示例程序的渲染步骤有一点不同。在这里首先要在颜色缓冲器中渲染一个二维的背景,代码如下:
 
void C_PrimitiveDemo::RenderPre2D() 
{
    p_BgSprite->Render2D();    //渲染背景
}
 
在dxframework的C_View::Render()函数中,如果C_View类的成员变量pre2D为true的话,即是意味着首先要在颜色缓冲器中渲染一个二维图。用作整个世界的背景。然后再真正渲染三维世界。
 
要想渲染一个图元,首先第一步出就要计算应用在这个图元上的世界矩阵才行。意思是指要计算出把图元从它自身的模型空间变换到世界空间中的变换矩阵。要计算出某图元的世界矩阵,首先要知道它变换后在世界空间中的位置坐标,以及它自身绕它自己的模型空间坐标轴旋转的角度。要知道变换后的位置坐标很好办,根据图元的最初摆放位置,和转盘转动的角度,利用简单的三角函数就能计算出来。运行时图元绕着自身的模型坐标轴旋转,旋转的度数是多少?代码如下:
 
void C_PrimitiveDemo::Render3D() 
{
    //用时钟计时值作为图元旋转的角度
    D3DXVECTOR3 objectAngle;
    //X分量存储了旋转的pitch角
    objectAngle.x = (float)gp_Timer->GetTime(); 
    //Y分量存储了旋转的yaw角
    objectAngle.y = (float)gp_Timer->GetTime(); 
    //Z分量存储了旋转的roll角
    objectAngle.z = (float)gp_Timer->GetTime(); 

    D3DXVECTOR3 pos;
    pos.y = 0;
    
    double tempTheta = theta;
    //逐个计算图元的在世界空间中的坐标
    for (int i = 0; i < 8; i++) 
    {
        tempTheta += i * 45;
        tempTheta = fmod(tempTheta,360);
        pos.x = (float)(5 * sin(tempTheta * D3DX_PI / 180.0));
        pos.z = (float)(5 * cos(tempTheta * D3DX_PI / 180.0));
 
知道了旋转角度和位置坐标之后,就可以生成把这个图元从模型空间变换到世界空间中去的代码了。根据旋转角度和位置坐标生成变换矩阵的函数如下:
 
D3DXMATRIX C_PrimitiveDemo::GetWorldMatrixRotateTranslate(D3DXVECTOR3 angle, D3DXVECTOR3 translation) 
{
    D3DXMATRIX matWorld, matTemp;
    D3DXMatrixScaling(&matWorld, 1.0f, 1.0f, 1.0f);
    //根据旋转角构造出旋转矩阵
    D3DXMatrixRotationYawPitchRoll(&matTemp, angle.y, angle.x, angle.z);
    D3DXMatrixMultiply(&matWorld, &matWorld, &matTemp);
    
    //再构造出平移矩阵
    D3DXMatrixTranslation(&matTemp, translation.x,translation.y, translation.z);

    //旋转矩阵和平移矩阵相乘便得到世界矩阵
    D3DXMatrixMultiply(&matWorld, &matWorld, &matTemp);
    return matWorld;
}
 
得到图元的世界矩阵之后便是可以开始渲染三维世界了,代码如下:
 
    //接上面的代码 C_PrimitiveDemo::Render3D() primitivedemo.cpp
    switch (i) 
    {
        case 0:
            p_Cube->Render3D(&GetWorldMatrixRotateTranslate(objectAngle,pos));
             break ;
        case 1:
              p_Disk->Render3D(&GetWorldMatrixRotateTranslate(objectAngle,pos));
            break ;
        case 2:
            p_Square->Render3D(&GetWorldMatrixRotateTranslate(objectAngle,pos));
            break ;
        case 3:
          p_Triangle->Render3D(&GetWorldMatrixRotateTranslate(objectAngle,pos));
            break ;
        case 4:
            p_Line->Render3D(&GetWorldMatrixRotateTranslate(objectAngle,pos));
            break ;
        case 5:
            p_Cylinder->Render3D(&GetWorldMatrixRotateTranslate(objectAngle,pos));
            break ;
        case 6:
            p_Cone->Render3D(&GetWorldMatrixRotateTranslate(objectAngle,pos));
            break ;
        case 7:
            p_Sphere->Render3D(&GetWorldMatrixRotateTranslate(objectAngle,pos));
            break ;
        default:
            throw Error( _T("C_PrimitiveDemo::Render3D():illegal index"));
        }
    }
}
 
渲染完图元之后,接着就是渲染字体了,代码如下:
 
void C_PrimitiveDemo::Render2D() 
{
    //渲染字体,是在渲染完三维世界之后渲染。
    p_TextFont->Render2D(shape, DVECTOR2(400, 400), WHITE,
        DVECTOR2(1,1), D3DFONT_CENTERED | D3DFONT_FILTERED);
}
 
渲染的代码分析完毕,整个demo的实现细节也分析了。接下来将分析下一个示例demo