在DirectX9.0中使用Mesh(2)

本章介绍D3DX库提供的与Mesh有关的接口、结构、函数。通过本章的学习,将能够加载复杂的3D模型,能够控制Mesh对象的精细程度。本章要达到的目标:

l          学习加载.x文件

l          理解使用渐进MeshProgressive Mesh)的好处和学习如何使用渐进Mesh接口ID3DXPMesh。将原文中的Progressive Mesh翻译为渐进网格,不知是否恰当

l          学习边界范围(Bounding Volume),以及如何使用D3DX函数创建边界范围

11.1.    关于ID3DXBuffer

这个接口贯穿整个D3DX库,需要对该接口有大体上的认识。ID3DXBufferD3DX用来管理连续内存块的结构,他只有两个方法:

l          LPVOID GetBufferPointer(); --返回数据块的首地址

l          DWORD GetBufferSize(); --返回缓冲区的大小,以字节为单位

例如,D3DXLoadMeshFromX函数就使用ID3DXBuffer返回Mesh对象的邻接信息。因邻接信息是DWORD数组,所以需要进行类型转换。如:

DWORD* info =(DWORD*)adjacencyInfo->GetBufferPointer();

D3DXMATERIAL* mtrls = (D3DXMATERIAL*)mtrlBuffer->GetBufferPointer();

又因为ID3DXBuffer是一个COM对象,所以,用完后,需要进行释放。

adjacencyInfo->Release();

mtrlBuffer->Release();

也可以使用如下函数创建一个空的ID3DXBuffer对象:

HRESULT WINAPI D3DXCreateBuffer(

    DWORD NumBytes,

    LPD3DXBUFFER *ppBuffer

);

其中参数的含义显而易见。例如,创建一个包含四个整型数的缓冲区:

ID3DXBuffer* buffer = 0;

D3DXCreateBuffer( 4 * sizeof(int), &buffer );

11.2.    X文件

使用D3DXCreate*函数,可以创建一些简单的几何体,如球、圆柱、立方体等。如果想通过手动设定顶点的方式创建较复杂的3D对象,你会发现这太麻烦了,简直无法做到!现在,可以使用很多种3D建模工具软件来完成这项枯燥工作,如3DS MAXLightWave 3DMaya等。使用这样的建模工具,可以在可视化的、交互的环境中设计复杂、逼真的模型,而且还有丰富的工具可用,使整个建模过程相当简单。这里的简单是相对于在“程序中手动设定顶点的方式建模”,实际上,这些建模工具还是相当复杂的,想得心应手的使用,可不是一朝一夕之功。

这些建模工具可以将所建立的模型的数据(几何信息,材质,动画等)保存到文件。我们需要从文件中分析提取需要的数据,然后应用到自己的3D程序中。有一种常用的文件格式,XFile,其扩展名为.x,较为简单,是Direct3D定义的文件格式,D3DX库提供了完整的支持,可满足一般的需要。

11.2.1. 加载一个.x文件

使用下面的函数加载存储在.x文件中的Mesh数据。它创建一个ID3DXMesh对象,然后从.x文件中读取Mesh的几何信息。

HRESULT WINAPI D3DXLoadMeshFromX(

    LPCTSTR pFilename,

    DWORD Options,

    LPDIRECT3DDEVICE9 pD3DDevice,

    LPD3DXBUFFER *ppAdjacency,

    LPD3DXBUFFER *ppMaterials,

    LPD3DXBUFFER *ppEffectInstances,

    DWORD *pNumMaterials,

    LPD3DXMESH *ppMesh

);

l          pFileName –.x文件的文件名

l          Options –创建Mesh的标志。详情可参考SDK文档中的D3DXMESH枚举类型。常用的几个标志如下:

n          D3DXMESH_32BIT –使用32位的顶点索引,默认为16

n          D3DXMESH_MANAGED –使用受控的内存缓冲池

n          D3DXMESH_WRITEONLY –缓冲区只可执行写操作

n          D3DXMESH_DYNAMIC –使用动态内存缓冲池

l          pD3DDevice –D3D设备指针

l          ppAdjacency –使用ID3DXBuffer返回Mesh的邻接信息,这是一个DWORD数组

l          ppMaterials –使用ID3DXBuffer返回Mesh的材质数据,这是一个D3DXMATERIAL类型数组

l          ppEffectInstances –使用ID3DXBuffer返回一个D3DXEFFECTINSTANCE结构数组

l          pNumMaterials –返回Mesh对象的材质数量,也就是通过ppMaterials返回的D3DXMATERIAL数组的元素数

l          ppMesh –返回ID3DXMesh对象

11.2.2. XFile材质

函数D3DXLoadMeshFromX的第七个参数返回Mesh对象的材质数量,第五个参数是D3DXMATERIAL的数组,包含Mesh的材质数据。D3DXMATERIAL结构的定义如下:

typedef struct D3DXMATERIAL {

    D3DMATERIAL9 MatD3D;

    LPSTR pTextureFilename;

} D3DXMATERIAL;

这个结构很简单,包含一个D3DMATERIAL9结构和一个以0字符结束的字符串的指针,表示相关联的纹理文件。. x文件并不包含纹理数据,只包含纹理文件的文件名。使用该函数加载.x文件后,还需要根据纹理文件的文件名手动加载纹理。

函数D3DXLoadMeshFromX返回的D3DXMATERIAL数组正好与Mesh对象的子集相对应。也就是说,第I个子集的材质纹理信息就存储在ppMaterials[I]中。

11.2.3. X文件的应用实例

这个例子相当的简单,它加载bigship1.x文件,这是DirectX SDK中的一个文件。这里只列出代码的主要框架。

ID3DXMesh* Mesh = 0;

vector<D3DMATERIAL9> Mtrls(0);

vector<IDirect3DTexture9*> Textures(0);

 

bool Setup()

{

    HRESULT hr = 0;

    //

    // Load the XFile data.

    //

    ID3DXBuffer* adjBuffer = 0;

    ID3DXBuffer* mtrlBuffer = 0;

    DWORD numMtrls = 0;

    hr = D3DXLoadMeshFromX(

        "bigship1.x",

        D3DXMESH_MANAGED,

        Device,

        &adjBuffer,

        &mtrlBuffer,

        0,

        &numMtrls,

        &Mesh);

    if(FAILED(hr))

    {

        ::MessageBox(0, "D3DXLoadMeshFromX() - FAILED", 0, 0);

        return false;

    }

    //

    // Extract the materials, load textures.

    //

    if( mtrlBuffer != 0 && numMtrls != 0 )

    {

        D3DXMATERIAL* mtrls=(D3DXMATERIAL*)mtrlBuffer->GetBufferPointer();

        for(int i = 0; i < numMtrls; i++)

        {

            // the MatD3D property doesn't have an ambient value

            // set when it’s loaded, so set it now:

            mtrls[i].MatD3D.Ambient = mtrls[i].MatD3D.Diffuse;

            // save the ith material

            Mtrls.push_back( mtrls[i].MatD3D );

            // check if the ith material has an associative

            // texture

            if( mtrls[i].pTextureFilename != 0 )

            {

                // yes, load the texture for the ith subset

                IDirect3DTexture9* tex = 0;

                D3DXCreateTextureFromFile(

                    Device,

                    mtrls[i].pTextureFilename,

                    &tex);

                // save the loaded texture

                Textures.push_back( tex );

            }

            else

            {

                // no texture for the ith subset

                Textures.push_back( 0 );

            }

        }

    }

    Release<ID3DXBuffer*>(mtrlBuffer); // done w/ buffer

    .

    . // Snipped irrelevant code to this chapter (e.g., setting up lights,

    . // view and projection matrices, etc.)

    .

    return true;

}

最后,渲染Mesh对象:

Device->Clear(0, 0, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER,0xffffffff, 1.0f, 0);

Device->BeginScene();

for(int i = 0; i < Mtrls.size(); i++)

{

    Device->SetMaterial( &Mtrls[i] );

    Device->SetTexture(0, Textures[i]);

    Mesh->DrawSubset(i);

}

Device->EndScene();

Device->Present(0, 0, 0, 0);

11.2.4. 创建顶点的法向量

有时.x文件不包含顶点的法向量,这时,如果使用光照,则需要手动计算顶点的法向量。对于接口ID3DXMesh和其父接口ID3DXBaseMesh,可以使用如下函数计算顶点的法向量:

HRESULT WINAPI D3DXComputeNormals(

    LPD3DXBASEMESH pMesh,

    const DWORD *pAdjacency

);

该函数将使用法向量的平均值作为顶点的法向量。如果提供了Mesh对象的邻接信息,则重复的顶点会被忽略;如果没有邻接信息,重复的顶点也会被重复计算。另外一点更加重要,需要计算法向量的Mesh对象的顶点格式必须包含D3DFVF_NORMAL标志。

如果.x文件中没有法向量数据,通过D3DXLoadMeshFromX函数创建的ID3DXMesh对象的顶点格式就不包含D3DFVF_NORMAL标志。因此,在计算法向量之前,必须使用D3DFVF_NORMAL标志复制Mesh对象。

// does the mesh have a D3DFVF_NORMAL in its vertex format?

if ( !(pMesh->GetFVF() & D3DFVF_NORMAL) )

{

    // no, so clone a new mesh and add D3DFVF_NORMAL to its format:

    ID3DXMesh* pTempMesh = 0;

    pMesh->CloneMeshFVF(

        D3DXMESH_MANAGED,

        pMesh->GetFVF() | D3DFVF_NORMAL, // add it here

        Device,

        &pTempMesh );

    // compute the normals:

    D3DXComputeNormals( pTempMesh, 0 );

    pMesh->Release(); // get rid of the old mesh

    pMesh = pTempMesh; // save the new mesh with normals

}

11.3.    渐进模型(Progressive Mesh

渐进MeshID3DXPMesh接口的对象,可以简化边缩减转换(Edge Collapse Transformations (ECT))。每次ECT都回减少一个顶点和一两个面。由于ECT过程是可逆的(他的逆过程叫顶点分裂),所以,可以通过逆过程将Mesh恢复到原始状态。当然,我们也无法得到比原始状态更精细的Mesh对象,最多只能将其恢复到原始状态。

Progressive Mesh和纹理中的mipmap十分相似。在较小的和远距离的对象上使用高分辨率的纹理纯粹是浪费,因为纹理的细节根本就表现不出来。对于Mesh对象也是一样,较小的距离较远的Mesh不需要太多的三角形,多了纯粹是浪费。所以,在渲染时,实在没有必要在这些根本表现不出来的地方浪费时间。

一种方法是,根据Mesh对象距离视点的距离调整其精细水准(LODLevel Of Detail)。当距离增加时,可降低LOD;反之,则增加LOD

这里只讨论ID3DXPMesh接口的用法,不讨论其实现细节。如果你感兴趣,可参考其它资料。

11.3.1. 生成一个渐进Mesh

使用下面的函数创建ID3DXPMesh对象:

HRESULT WINAPI D3DXGeneratePMesh(

    LPD3DXMESH pMesh,

    const DWORD *pAdjacency,

    const D3DXATTRIBUTEWEIGHTS *pVertexAttributeWeights,

    const FLOAT *pVertexWeights,

    DWORD MinValue,

    DWORD Options,

    LPD3DXPMESH *ppPMesh

);

l          pMesh –输入的普通的Mesh对象

l          pAdjacency –Mesh对象的邻接信息,这是一个DWORD数组

l          pVertexAttributeWeights –结构D3DXATTRIBUTEWEIGHTS的数组,元素个数为pMesh->GetNumVertices(),表示顶点的属性的权。在简化Mesh对象时,权值决定一个顶点被删除的可能性大小。该参数可以设为NULL,这时顶点使用默认的权值。

l          pVertexWeights –顶点的权,是float数组,元素个数是pMesh->GetNumVertices(),用于决定顶点在简化时被删除的可能性的大小。该参数也可设为NULL,这时,顶点默认的权值为1.0f

l          MinValue –在简化Mesh时,顶点或者三角形数的最小个数。该参数是必要的,而且与顶点权值和顶点属性权值有关系,最终也许达不到该数值。

l          Options –只能取D3DXMESHSIMP枚举类型中的一个值:

n          D3DXMESHSIMP_VERTEX –上一个参数MinValue指顶点数

n          D3DXMESHSIMP_FACE –上一个参数MinValue指三角形数

l          ppPMesh –返回生成的渐进Mesh

11.3.2. 顶点的属性权

typedef struct _D3DXATTRIBUTEWEIGHTS {

    FLOAT Position;

    FLOAT Boundary;

    FLOAT Normal;

    FLOAT Diffuse;

    FLOAT Specular;

    FLOAT Texcoord[8];

    FLOAT Tangent;

    FLOAT Binormal;

} D3DXATTRIBUTEWEIGHTS, *LPD3DXATTRIBUTEWEIGHTS;

通过这个结构,可以为顶点的每个属性指定一个权值,0.0表示属性没有权。权值越高,在简化时,越不易被删除。默认的权值如下:

D3DXATTRIBUTEWEIGHTS AttributeWeights;

AttributeWeights.Position = 1.0;

AttributeWeights.Boundary = 1.0;

AttributeWeights.Normal = 1.0;

AttributeWeights.Diffuse = 0.0;

AttributeWeights.Specular = 0.0;

AttributeWeights.Tex[8] = {0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0};

一般情况下,推荐使用默认的权值,除非你认为非常有必要使用不同的权值。

11.3.3. ID3DXPMesh的方法

接口ID3DXPMesh继承自ID3DXBaseMesh,下面介绍一些常用的方法。

l          DWORD GetMaxFaces(VOID); --返回Mesh的最大三角形数

l          DWORD GetMaxVertices(VOID); --返回Mesh的最大顶点数

l          DWORD GetMinFaces(VOID); --返回Mesh的最少三角形数

l          DWORD GetMinVertices(VOID); --返回Mesh的最少顶点数

l          HRESULT SetNumFaces(DWORD Faces); --设置Mesh的三角形数。例如,假定Mesh现在有50个三角形,而想将其简化为30个三角形,则调用pmesh->SetNumFaces(30)。调整后的三角形数可能并不是我们设定的个数,因为PMesh的三角形数还有最大和最少的限制。

l          HRESULT SetNumVertices(DWORD Vertices); --设置PMesh的顶点个数。例如,假设现在PMesh20个顶点,而为了增加其精细程度将顶点数增为40个,则只需调用pmesh->SetNumVertices(40)。与三角形数一样,最终的结果可能不是我们指定的数值,同样有最大最少个数的限制。

l          HRESULT TrimByFaces(

    DWORD NewFacesMin,

    DWORD NewFacesMax,

    DWORD *rgiFaceRemap,

    DWORD *rgiVertRemap

); --该方法设定PMesh三角形数的最大最小值。新的最大最小值必须在当前的最大最小值之间,即必须在[GetMinFaces()GetMaxFaces()]内。同时,该方法还将返回三角形和顶点的重影射信息。

l          HRESULT TrimByVertices(

    DWORD NewVerticesMin,

    DWORD NewVerticessMax,

    DWORD *rgiFaceRemap,

    DWORD *rgiVertRemap

); --该方法与上面的方法相似。

11.3.4. 应用举例:Progressive Mesh

这个例子与前面的XFile例子相似,只是其中使用ID3DXPMesh接口。

与前例相似,我们使用如下的全局变量:

ID3DXMesh* SourceMesh = 0;

ID3DXPMesh* PMesh = 0; // progressive mesh

vector<D3DMATERIAL9> Mtrls(0);

vector<IDirect3DTexture9*> Textures(0);

在创建Progressive Mesh之前,需要使用ID3DXMesh接口加载.x文件:

HRESULT hr = 0;

// ...Load XFile data into SourceMesh snipped.

//

// ...Extracting materials and textures snipped.

 

//

// Generate the progressive mesh.

//

hr = D3DXGeneratePMesh(

    SourceMesh,

    (DWORD*)adjBuffer->GetBufferPointer(), // adjacency

    0, // default vertex attribute weights

    0, // default vertex weights

    1, // simplify as low as possible

    D3DXMESHSIMP_FACE, // simplify by face count

    &PMesh);

Release<ID3DXMesh*>(SourceMesh); // done w/ source mesh

Release<ID3DXBuffer*>(adjBuffer); // done w/ buffer

if(FAILED(hr))

{

    ::MessageBox(0, "D3DXGeneratePMesh() - FAILED", 0, 0);

    return false;

}

通常,因为顶点和顶点属性权值的缘故,很难将Mesh简化到只有一个三角形的程度,但是,如果指定将Mesh简化到一个三角形的程度,则可以将Mesh简化到解析度最低的程度。

现在,渐进Mesh已经生成了,但是,如果直接渲染,则Mesh的解析度此时最低。如果想渲染全解析度的PMesh,首先需要设置其三角形数:

// set to original (full) detail

DWORD maxFaces = PMesh->GetMaxFaces();

PMesh->SetNumFaces(maxFaces);

在渲染PMesh时,我们使用键盘输入控制其解析度:A键将增加解析度,S键减小解析度。

// Get the current number of faces the pmesh has.

int numFaces = PMesh->GetNumFaces();

// Add a face, note the SetNumFaces() will automatically

// clamp the specified value if it goes out of bounds.

if( ::GetAsyncKeyState('A') & 0x8000f )

{

    // Sometimes we must add more than one face to invert

    // an edge collapse transformation because of the internal

    // implementation details of the ID3DXPMesh interface. In

    // other words, adding one face may possibly result in a

    // mesh with the same number of faces as before. Thus to

    // increase the face count we may sometimes have to add

    // two faces at once.

    PMesh->SetNumFaces(numFaces + 1);

    if(PMesh->GetNumFaces() == numFaces)

        PMesh->SetNumFaces(numFaces + 2);

}

// Remove a face, note the SetNumFaces() will automatically

// clamp the specified value if it goes out of bounds.

if(::GetAsyncKeyState('S') & 0x8000f)

    PMesh->SetNumFaces(numFaces - 1);

上面的方法直截了当,只是增加三角形数时,有时需要增加两个来满足ECT的需要。

最后,使用和渲染ID3DXMesh同样的方法渲染ID3DXPMesh。另外,为了更加直观的观察PMesh的三角形数的变化情况,使用黄色材质在线框模式(Wireframe Mode)下渲染Mesh的三角形。

Device->Clear(0, 0, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER,0xffffffff, 1.0f, 0);

Device->BeginScene();

for(int i = 0; i < Mtrls.size(); i++)

{

    Device->SetMaterial( &Mtrls[i] );

    Device->SetTexture(0, Textures[i]);

    PMesh->DrawSubset(i);

    // draw wireframe outline

    Device->SetMaterial(&d3d::YELLOW_MTRL);

    Device->SetRenderState(D3DRS_FILLMODE, D3DFILL_WIREFRAME);

    PMesh->DrawSubset(i);

    Device->SetRenderState(D3DRS_FILLMODE, D3DFILL_SOLID);

}

Device->EndScene();

Device->Present(0, 0, 0, 0);

11.4.    对象的边界范围

有时,需要计算Mesh对象的边界范围,常用的有两种类型:立方体和球。也有使用其它方法的,如圆柱体、椭球体、菱形体、太空舱形等。这里,我们只讨论立方体和球体两种边界形式。

边界盒或边界球常用来加速多物体间的可视范围测试、碰撞检测等。如果一个Mesh的边界盒/球不可见,就可认为Mesh也不可见。检测边界盒/球是否可见比检测Mesh中所有的三角形是否可见要方便得多。在碰撞检测中,如果一枚导弹点火起飞,我们需要检测他是否撞到了同一个场景中的目标。由于这些对象全是大量的三角形构成,我们可以依次检测每个对象的每个三角形,检测导弹(可以使用数学模型中的射线)是否撞到了这些三角形。这个方法需要进行多次的射线/三角形交点的运算。较好的方法是使用边界盒或边界球,计算射线与场景中的每个对象的边界盒/边界球的交点。如果射线与对象的边界范围相交,可以认为该对象被击中了。这是一个公平的近似方法,如果需要更高的精度,可以用边界范围法先去除那些明显不会相撞的对象,然后用更精确地方法检测很可能相撞的对象。如果边界范围检测发现相撞,则该对象就很有可能相撞。

D3DX库提供了计算Mesh对象边界盒/球的函数。这些函数使用顶点数组作为输入计算边界盒/球,可以使用各种顶点格式:

HRESULT WINAPI D3DXComputeBoundingSphere(

    const D3DXVECTOR3 *pFirstPosition,

    DWORD NumVertices,

    DWORD dwStride,

    D3DXVECTOR3 *pCenter,

    FLOAT *pRadius

);

l          pFirstPosition –顶点数组的地址,顶点的第一个向量需要是顶点的位置坐标

l          NumVertices –顶点的数目

l          dwStride –顶点大小,以字节为单位。因顶点中有很多附加数据,如法向量、纹理坐标等,计算边界范围不需要这些数据,所以,需要知道跳过多少数据才能找到下一个顶点的坐标。

l          pCenter –返回边界范围的中心

l          pRadius –返回边界球的半径

HRESULT WINAPI D3DXComputeBoundingBox(

    const D3DXVECTOR3 *pFirstPosition,

    DWORD NumVertices,

    DWORD dwStride,

    D3DXVECTOR3 *pMin,

    D3DXVECTOR3 *pMax

);

前三个参数与计算边界球的函数相同;后两个参数返回边界盒的最小和最大点。

11.4.1. 边界检测类型

为了使边界检测易于使用,我们实现几个辅助的数据结构:

struct BoundingBox

{

    BoundingBox();

    bool isPointInside(D3DXVECTOR3& p);

    D3DXVECTOR3 _min;

    D3DXVECTOR3 _max;

};

struct BoundingSphere

{

    BoundingSphere();

    D3DXVECTOR3 _center;

    float _radius;

};

 

BoundingBox::BoundingBox()

{

    // infinite small bounding box

    _min.x = FLT_MAX;

    _min.y = FLT_MAX;

    _min.z = FLT_MAX;

    _max.x = -FLT_MAX;

    _max.y = -FLT_MAX;

    _max.z = -FLT_MAX;

}

bool BoundingBox::isPointInside(D3DXVECTOR3& p)

{

    // is the point inside the bounding box?

    if (p.x >= _min.x && p.y >= _min.y && p.z >= _min.z &&

        p.x <= _max.x && p.y <= _max.y && p.z <= _max.z)

    {

        return true;

    }

    else

    {

        return false;

    }

}

BoundingSphere::BoundingSphere()

{

    _radius = 0.0f;

}

11.4.2. 边界范围应用举例

该例子演示D3DXComputeBoundingSphereD3DXComputeBoundingBox函数的用法。程序首先加载一个.x文件,然后计算Mesh的边界盒/球。代码中创建两个ID3DXMesh对象,分别使用边界盒和边界球。最后,分别渲染他们。

这个例子很简单,这里只给出有关边界范围的代码:

bool ComputeBoundingSphere(

    ID3DXMesh* mesh, // mesh to compute bounding sphere for

    BoundingSphere* sphere) // return bounding sphere

{

    HRESULT hr = 0;

    BYTE* v = 0;

    mesh->LockVertexBuffer(0, (void**)&v);

    hr = D3DXComputeBoundingSphere(

        (D3DXVECTOR3*)v,

        mesh->GetNumVertices(),

        D3DXGetFVFVertexSize(mesh->GetFVF()),

        &sphere->_center,

        &sphere->_radius);

    mesh->UnlockVertexBuffer();

    if( FAILED(hr) )

        return false;

    return true;

}

bool ComputeBoundingBox(

    ID3DXMesh* mesh, // mesh to compute bounding box for

    BoundingBox* box) // return bounding box

{

    HRESULT hr = 0;

    BYTE* v = 0;

    mesh->LockVertexBuffer(0, (void**)&v);

    hr = D3DXComputeBoundingBox(

        (D3DXVECTOR3*)v,

        mesh->GetNumVertices(),

        D3DXGetFVFVertexSize(mesh->GetFVF()),

        &box->_min,

        &box->_max);

    mesh->UnlockVertexBuffer();

    if( FAILED(hr) )

        return false;

    return true;

}

类型转换(D3DXVECTOR3*)v假定顶点坐标在顶点结构的开头位置,一般都是如此。

11.5.    总结

l          现在,我们可以用3D建模软件导出的.x文件构建复杂的Mesh对象。使用D3DXLoadMeshFromX函数取得ID3DXMesh对象,就可以在自己的应用程序中自由使用了。

l          使用ID3DXPMesh接口表示的渐进Mesh,可以控制其精细程度。可以根据对象在场景中的突出程度调整PMesh的精细程度。

l          我们可以使用D3DXComputeBoundingSphereD3DXComputeBoundingBox函数计算Mesh对象的边界。边界范围很有用,其接近对象真实的边界,可加速碰撞检测等的计算。

 

 


你可能感兴趣的:(struct,buffer,float,transformation,textures,winapi)