Game Programming with DirectX -- 07[基本3D模型构造]
第七集 基本3D模型构造
7.1 顶点索引
7.1.1 金字塔模型
game1 project中使用的金字塔是由六个三角形的三角形带组成的, 金字塔一共有5个顶点, 如图7.1左边所示.
图7.1
由5个顶点构成的金字塔的三角形带为图右边的顺序, 0-1-2, 1-2-3, ..., 5-6-7, 其中有3个顶点是重复的.
7.1.2 Indices buffer
实际由5个顶点组成的金字塔在IDirect3DVertexBuffer9表示需要8个顶点, 每多一个顶点, 渲染时计算量就会增加. 顶点索引的作用就是减少顶点的重复定义, 顶点索引使用IDirect3DIndexBuffer9表示, IDirect3DIndexBuffer9中存储的数值是相应的在IDirect3DVertexBuffer9中顶点数组序列号, 金字塔用顶点索引表示如下,
IDirect3DIndexBuffer9 IDirect3DVertexBuffer9
[ 0 ] [ 0.0, 8.0, 0.0 ]
[ 1 ] [ 6.0, 0.0, -6.0 ]
[ 2 ] [ -6.0, 0.0, -6.0 ]
[ 3 ] [ -6.0, 0.0, 6.0 ]
[ 0 ] [ 6.0, 0.0, 6.0 ]
[ 4 ]
[ 1 ]
[ 3 ]
IDirect3DIndexBuffer9是由IDirect3DDevice9接口中的CreateIndexBuffer(...)函数创建的, 创建方式和参数同IDirect3DVertexBuffer9类似, 要注意顶点索引根据实际情况可使用32位的索引(对应数值类型是DWORD), 16位的索引(对应数值类型是WORD).
当使用顶点索引后, 图形绘制的调用函数有点小改动, 如绘制金字塔,
// m_pD3DDev --- pointer of IDirect3DDevice9
// m_pD3DVBuffer --- pointer of IDirect3DVertexBuffer9
// m_pD3DIBuffer --- pointer of IDirect3DIndexBuffer9
m_pD3DDev->SetStreamSource(0, m_pD3DVBuffer, 0, sizeof(MYVERTEX));
m_pD3DDev->SetFVF(D3DFVF_MYVERTEX);
// 绘制图形前要告诉IDirect3DDevice9现在用到的顶点索引
m_pD3DDev->SetIndices(m_pD3DIBuffer);
// 绘制图形的函数由DrawIndexedPrimitive替换DrawPrimitive
m_pD3DDev->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, 0, 0, 5, 0, 6);
具体的解释顶点和顶点索引的关系的可以在DirectX9c SDK中的Rendering from Vertex and Index Buffers主题中找到.
7.1.3 Indices buffer VS vertex buffer
这里我们要注意在光照(或纹理映射)条件下, 由于多个面共用的顶点在各各面的法向量(纹理坐标)不同, 还是需要存储同一顶点的不同法向量(纹理坐标)的多个实例.
例如表示一个简单的立方体模型, 只须8个顶点数值和相应的顶点索引. 实际要考虑物体纹理映射或光照计算, 这时8个顶点是不够的, 如图7.2立方体, 我们计算在光照条件下时, 每个顶点在不同面的法向量不同, 所以每个顶点实际需存储3次, 一个立方体需 3 * 8 = 24 个顶点数值, 如果不考虑顶点索引, 每个面有 6 个顶点数值, 共需 6 * 6 = 36 个顶点数值.
图7.2
game5 project中矩形在有纹理映射条件下无顶点索引时有 36 个顶点数值.
7.2 基本3D几何模型
7.2.1 圆柱模型
圆柱模型的底面和顶都是由三角扇形组成, 简单的侧面是由一三角形带组成, 侧面也可用多层三角形带组成. 像切蛋糕一样, 圆柱被分成块状, 分的越细模型越精确. 可参考图7.3, 在构造时, 模型局部坐标系的中心是圆柱的中心.
7.2.2 圆锥模型
圆锥模型实际就是去掉顶部的圆柱, 侧面三角形带有一共同的顶点坐标. 其实圆柱模型的构造过程可以生成圆台, 棱台和棱锥几何模型.
图7.3
7.2.3 球模型
图7.4左边是两个四棱锥组成的正8面体, 现在从这个正8面体导出球的模型.
通过正8面体内正方形的对角线将正8面体切成4块, 其中的角a为90度; 如果称正8面体为球的话, 正8面体内正方形的四条边围成的就是赤道, 球半径R为正方形的对角线长的一半, 那么正8面体是一个4经度2纬度的球了.
图7.4
现在在切平面上寻找一个点C, 使得三角形ABC为等边三角形, 并且C到中心O的距离是R, 这样就形成图7.4右边的4经度4纬度的球. 如果再寻找点D, E, 使得三角形DAC, ECB为等边三角形, 并且D, E到中心O的距离是R, ... , 按以上方法, 类似C的点越多, 切平面中的图形越接近圆. ------ 球的层次数
现在再增加赤道切平面上的点C', 使得三角形AB'C'为等边三角形, 并且C'到中心O的距离是R, ... ------ 球的分块数
由上的方法, 正8面体就趋向于它的外接球了. 所以, 只要知道球的半径, 层次数, 分块数, 我们就可以构造出球的模型.
图7.5
如图7.5, 以正方形的对角线分别为X, Z轴, 交点为原点建立局部坐标系, B点的坐标是( Rsina * sinb, Rcosa, Rsina * cosb), 使用三角形带(DirectX Graphics推荐)实现,为代码简单, 将ABC, ACD, ADE, AEB看成是四边形ABCA, ACDA, ADEA, AEBA, 底部的也同样. 这样球的顶点数为PartCount * (LayerCount + 1), 图7.5中4经度4纬度的球PartCount = 4, LayerCount = 4, 每个块半切面有(LayerCount + 1) = 5个顶点, 共有PartCount = 4个半块切面, 所以顶点数PartCount * (LayerCount + 1) = 4 * 5 = 20(有6个重复点).
FLOAT fPlaneRad = D3DX_PI / (m_nPart >> 1); // (2PI / PartCount) = b 弧度增量
FLOAT fApeakRad = D3DX_PI / m_nLayer; // (PI / LayerCount) = a 弧度增量
FLOAT fuMap = (FLOAT)(1.0 / m_nPart); // 纹理坐标 U 的增量
FLOAT fvMap = (FLOAT)(1.0 / m_nLayer); // 纹理坐标 V 的增量
FLOAT nx = 0.0;
FLOAT ny = 0.0;
FLOAT nz = 0.0;
FLOAT fPlane = 0.0;
WORD nIndex = 0;
WORD nLayerIndex = 0;
for (INT i = 0; i <= m_nLayer; i++)
{
fPlane = sinf(fApeakRad * i);
ny = cosf(fApeakRad * i);
for (INT j = 0; j < m_nPart; j++)
{
nx = (FLOAT)fPlane * sinf(fPlaneRad * j);
nz = (FLOAT)fPlane * cosf(fPlaneRad * j);
pVertex->x = FLOAT(m_fRadius * nx);
pVertex->y = FLOAT(m_fRadius * ny);
pVertex->z = FLOAT(m_fRadius * nz);
pVertex->nx = nx;
pVertex->ny = ny;
pVertex->nz = nz;
pVertex->tu = FLOAT(fuMap * j);
pVertex->tv = FLOAT(fvMap * i);
pVertex++;
if (i < m_nLayer)
{
*pIndex = nIndex; // A 点索引
pIndex++;
*pIndex = WORD(nIndex + m_nPart); // B 点索引
pIndex++;
nIndex++;
}
}
if (i < m_nLayer)
{
*pIndex = nLayerIndex;
pIndex++;
nLayerIndex += (WORD)m_nPart;
*pIndex = nLayerIndex;
pIndex++;
}
}
代码以层次关系填写顶点值和顶点索引, 图7.5顶点数值为AAAA-BCDE-FGHI-JKLM-NNNN,
(1), 内循环第一次(第一层)索引值为ABACADAE; 内循环第一次结束ABACADAE + AB.
(2), 内循环第二次(第二层)索引值为ABACADAE + AB + BFCGDHEI; 内循环第二次结束ABACADAE + AB + BFCGDHEI + BF.
......
我们发现在层之间跳转时会出现无效的三角形ABB, BBF, 每增加一层就会有多余的2个无效的三角形, 其中最顶层和最底层只有一个.
每层索引为2 * PartCount + 2(注意加2),总共索引2 * (PartCount + 1) * LayerCount = 40, 三角形个数2 * (PartCount + 1) * LayerCount - 2(注意减2) = 38, 其中有6个无效三角形.
7.3 基本地形模型
7.3.1 简单地形模型
最最简单的地形就是四边形平面了(最好是矩形或正方形), 用两个三角形组成. 在复杂一点的地形都是在四边形平面地形基础上加强的.
想象一下家里N年前刚铺木地板时的平整光滑, 经过N年, 平整的木地板有的地方被砸凹了, 有的泡水太长拱上来了...., 总之凹凸不平了.
有高低起伏的地形可仿造家中木地板的演变得到, 先把四边形平面分成很多的小四边形平面(最好是矩形或正方形), 这样地形就需要行数row, 纵数column及小四边形的边长来描述划分的密度了, 图7.6上边是一个4 * 4的平整地形.
图7.6
现在来描述地形的高低信息, 图7.6中, 只要相应的改变各点的高度值, 就可以形成高低起伏的地形了, 高度值可用一个小于1的随机数值 * 最高高度得到(游戏一般是用高度图描述的), 图7.6下边是4 * 4的平整地形通过改变顶点高度值得到的.
用这种方式形成的地形简单快速, 顶点数为(row + 1) * (column + 1), 图7.6中有25个顶点. 每个四边形有两个三角形, 总共三角形个数row * column * 2, 每个三角形有三个顶点, 总共索引row * column * 2 * 3.
如图7.6建立坐标系, 各顶点的顺序按图中的标号, 顶点索引的实现为,
// m_pIndex 为指向顶点索引数组的指针
WORD v1 = 0; // 从第0个顶点开始
WORD v2 = m_nCols + 1; // column + 1 = 5
WORD v3 = v2 + 1; // column + 1 + 1 = 6
for (INT a = 0; a < m_nRows; a++)
{
for (INT b = 0; b < m_nCols; b++)
{
m_pIndex[i] = v1;
m_pIndex[i + 1] = v2;
m_pIndex[i + 2] = v3;
m_pIndex[i + 3] = v1;
m_pIndex[i + 4] = v3;
m_pIndex[i + 5] = v1 + 1;
v1++;
v2++;
v3++;
i += 6;
}
v1++;
v2++;
v3++;
}
7.4 基本3D模型的例子
7.4.1 game6 project代码更新
game6 project中, 用基本地形构造了埃及的金字塔群, 为地形加入了一个地形类CD9Terrain.
---------------------------------------------------------------
// 根据地形row和column计算顶点数, 顶点索引数和三角形个数
// nMaxHeight 为地形最高高度, fSide为小四边形(正方形)的边长
HRESULT CD9Terrain::Init(INT nRows, INT nCols, INT nMaxHeight, FLOAT fSide)
{
SAFERELEASE( m_pD3DIBuffer );
SAFERELEASE( m_pD3DVBuffer );
m_nRows = nRows;
m_nCols = nCols;
m_nMaxHeight = nMaxHeight;
m_nVertices = (nRows + 1) * (nCols + 1);
m_nTriangle = nRows * nCols * 2;
m_nIndices = m_nTriangle * 3;
m_fSide = fSide;
if (SUCCEEDED(InitVertexBuffer()))
{
if (SUCCEEDED(InitIndexBuffer()))
{
return UpdateD3DVertex();
}
}
return E_FAIL;
}
---------------------------------------------------------------
7.4.2 game6 project说明
例子中地形纹理映射和顶点的法向量计算都是前几集的内容.
7.4.3 game7 project代码更新
game7是比较综合的例子 --- 地月系统, 例子中加入了构造圆柱, 圆锥和球的3D模型的类, 其实这些模型DirectX Graphics有专门的实用函数来构造, 这里只是为后面的天空顶技术热热脑子...
---------------------------------------------------------------
// 圆锥构造
HRESULT CD9Cone::UpdateD3DVertex()
{
LPWORD pIndex = NULL;
MYVERTEXTEX* pVertex = NULL;
INT nIndicesSize = m_nIndices * sizeof(WORD);
INT nVerticesSize = m_nVertices * sizeof(MYVERTEXTEX);
FLOAT fRadian = D3DX_PI / (m_nPart >> 1);
FLOAT fTextureMap = FLOAT(1.0 / m_nPart);
FLOAT fHalfH = FLOAT(m_fHeight / 2);
FLOAT fHalfH1 = FLOAT(0 - fHalfH);
FLOAT ny = sinf(atan(m_fRadius / m_fHeight));
FLOAT nx = 0.0;
FLOAT nz = 0.0;
FLOAT x = 0.0;
FLOAT z = 0.0;
FLOAT u = 0.0;
WORD nIndex = 0;
if (SUCCEEDED(m_pD3DIBuffer->Lock(0, nIndicesSize, (LPVOID*)&pIndex, 0)))
{
if (FAILED(m_pD3DVBuffer->Lock(0, nVerticesSize, (LPVOID*)&pVertex, 0)))
{
m_pD3DIBuffer->Unlock();
return E_FAIL;
}
// side
for (INT i = 0; i < m_nPart; i++)
{
nx = sinf(fRadian * i);
nz = cosf(fRadian * i);
x = m_fRadius * nx;
z = m_fRadius * nz;
u = FLOAT(fTextureMap * i);
pVertex->x = 0;
pVertex->y = fHalfH;
pVertex->z = 0;
pVertex->nx = nx;
pVertex->ny = ny;
pVertex->nz = nz;
pVertex->tu = u;
pVertex->tv = 0.0;
pVertex++;
pVertex->x = x;
pVertex->y = fHalfH1;
pVertex->z = z;
pVertex->nx = nx;
pVertex->ny = ny;
pVertex->nz = nz;
pVertex->tu = u;
pVertex->tv = 1.0;
pVertex++;
*pIndex = nIndex;
pIndex++;
nIndex++;
*pIndex = nIndex;
pIndex++;
nIndex++;
*pIndex = nIndex + 1;
pIndex++;
}
*(pIndex - 1) = 1;
// bottom
pVertex->x = 0.0;
pVertex->y = fHalfH1;
pVertex->z = 0.0;
pVertex->nx = 0.0;
pVertex->ny = -1.0;
pVertex->nz = 0.0;
pVertex->tu = 0.5;
pVertex->tv = 0.5;
pVertex++;
for (INT j = 0; j <= m_nPart; j++)
{
nx = sinf(fRadian * j);
nz = cosf(fRadian * j);
pVertex->x = m_fRadius * nx;
pVertex->y = fHalfH1;
pVertex->z = m_fRadius * nz;
pVertex->nx = 0.0;
pVertex->ny = -1.0;
pVertex->nz = 0.0;
pVertex->tu = FLOAT(nx * 0.5 + 0.5);
pVertex->tv = FLOAT(nz * 0.5 + 0.5);
pVertex++;
}
m_pD3DVBuffer->Unlock();
m_pD3DIBuffer->Unlock();
m_bInit = TRUE;
return S_OK;
}
return E_FAIL;
}
---------------------------------------------------------------
7.4.4 game7 project说明
使用IDirect3DDevice9的SetRenderState(D3DRS_FILLMODE, D3DFILL_WIREFRAME)函数来查看模型的网格.
第七集 小结
这一集我们学习了要进行DirectX Graphics 3D编程中的基本模型的构造过程.