Direct3D网格(二)

ID3DXBuffer

ID3DXBuffer接口是一种泛型数据结构,该接口为D3DX库锁使用,可将数据存储在一个连续的内存块中,该接口只有俩个方法。为了保持该接口的通用性,该接口使用了void类型指针,所以使用时需要对该缓存进行强制类型转换。由于ID3DXBuffer是一个COM对象,该接口在使用完毕之后必须将其释放,以防止内存泄漏。

//返回指向缓存中数据起始位置的指针
LPVOID GetBufferPointer();
//返回缓存的大小,单位为字节
DWORD GetBufferSize();

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

可用D3DXCreateBuffer函数来创建一个空的ID3DXBuffer对象

HRESULT WINAPI 
    D3DXCreateBuffer(
    DWORD NumBytes, 
    LPD3DXBUFFER *ppBuffer
);

ID3DXBuffer* buffer=0;
D3DXCreateBuffer(4*sizeof(int),&buffer);

XFile

当我们通过手工指定顶点数据来创建3D物体时,将是一个相当枯燥的任务,为了减轻构建3D物体这种繁重的工作,人们开发了3D建模工具的专业应用程序,这些工具能够将模型数据导成文件格式为XFile(扩展名为.X)文件,XFile之所以方便,最主要原因是它是DirectX定义的格式,得到了D3DX库的支持。

加载XFile文件

可以使用D3DXLoadMeshFromX加载,该方法创建了一个ID3DXMesh对象并将XFile中的几何数据加载到该对象中。

HRESULT D3DXLoadMeshFromX(
	LPCWSTR pFilename,
	DWORD Options,
	LPDIRECT3DDEVICE9 pD3DDevice,
	LPD3DXBUFFER *ppAdjacency,
	LPD3DXBUFFER *ppMaterials,
	LPD3DXBUFFER *ppEffectInstances,
	DWORD *pNumMaterials,
	LPD3DXMESH *ppMesh
);

pFileName:所要加载的XFile文件名
Options:创建网格时所使用的创建标记,枚举为D3DXMESH类型
pDevice:设备指针
ppAdjacency:返回一个ID3DXBuffer对象,该对象包含了一个描述了该网格对象的邻接信息的DWORD类型的数组
ppMaterials:返回一个ID3DXBuffer对象,该对象包含一个存储了该网格的材质数据的D3DXMATERIAL类型的结构数组
ppEffectInstances:返回一个ID3DXBuffer对象,该对象包含了一个D3DXEFFECTINSTANCE结构,可以通过将该参数指定为0而将其忽略
pNumMaterials:返回网格网格中材质数目(ppMaterials输出数组的元素个数)
ppMesh:返回所创建并填充了XFile几何数据的ID3DXMesh对象

XFile材质

第二个参数为一个指向NULL结尾的字符串指针,该字符串指定了与网格相关的纹理文件名,XFile文件中并未存储纹理数据,它只包含了纹理图像文件名,该文件名是对包含了实际纹理数据的纹理图像的引用,所以使用D3DXLoadMeshFromX函数加载一个XFile文件后,我们必须根据指定的纹理文件名加载纹理数据

typedef struct _D3DXMATERIAL
{
	D3DMATERIAL9  MatD3D;
	LPSTR         pTextureFilename;
} D3DXMATERIAL;

D3DXLoadMeshFromX函数载入XFile数据后返回的D3DXMATERIAL结构数组中的第i项就与第i个子集相对应,所以将各子集按照0、1、2...n-1进行标记,n是子集和材质的总数,这样就可以用循环来对子集进行遍历绘制,从而完成网格的绘制。

XFile例程

ID3DXMesh* Mesh = 0;
std::vector<D3DMATERIAL9> Mtrls(0);
std::vector<IDirect3DTexture9*> Textures(0);

//加载XFile文件
bool SetUpXFile()
{
	HRESULT hr = 0;
	ID3DXBuffer* adjBuffer = 0;
	ID3DXBuffer* mtrlBuffer = 0;
	DWORD numMtrls = 0;

	hr = D3DXLoadMeshFromX(L"bigshipl.x", D3DXMESH_MANAGED, Device, &adjBuffer, &mtrlBuffer, 0, &numMtrls, &Mesh);
	if (FAILED(hr))
	{
		::MessageBox(0, L"load Fail", 0, 0);
		return false;
	}

	//加载XFile数据后,必须遍历D3DXMATERIAL数组中的元素,并加载该网格锁引用的纹理数据

	if (mtrlBuffer != 0 && numMtrls != 0)
	{
		D3DXMATERIAL* mtrls = (D3DXMATERIAL*)mtrlBuffer->GetBufferPointer();
		for (int i = 0; i < numMtrls; ++i)
		{
			mtrls[i].MatD3D.Ambient = mtrls[i].MatD3D.Diffuse;
			Mtrls.push_back(mtrls[i].MatD3D);

			if (mtrls[i].pTextureFilename != 0)
			{
				IDirect3DTexture9* tex = 0;
				D3DXCreateTextureFromFileA(Device, mtrls[i].pTextureFilename, &tex);
				Textures.push_back(tex);
			}
			else
			{
				Textures.push_back(0);
			}
		}
	}

	d3d::Release<ID3DXBuffer*>(mtrlBuffer);
	return true;
}

bool DisplayXFile(float timeDelta)
{
	if (Device)
	{
		static float y = 0.0f;
		D3DXMATRIX yRot;
		D3DXMatrixRotationY(&yRot, y);
		y += timeDelta;

		if (y >= 6.28f)
			y = 0.0f;

		D3DXMATRIX World = yRot;
		Device->SetTransform(D3DTS_WORLD, &World);

		//render

		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);
	}
	return true;
}

生成顶点法线

XFile文件中有可能没有存放顶点的法向量,为了使用光照需要手工计算每个顶点的法向量,可以使用D3DXComputeNormals来产生任意网格的顶点法向量。

HRESULT WINAPI D3DXComputeNormals(
       LPD3DXBASEMESH pMesh,
       CONST DWORD *pAdjacency
);

该函数通过法向量平均的方法生成顶点的法向量,如果提供了邻接信息,重叠的顶点就会被剔除,如果没有提供邻接信息,则重叠顶点的法向量由该顶点所依附的各面在该点的局部法向量取平均而得到,重要的第一点是传入的pMesh的顶点格式中必须包含标记D3DFVF_NORMAL

如果一个XFile文件不含顶点法线数据,则通过函数D3DXLoadMeshFromX所创建出的网格对象在顶点格式中将不包含D3DFVF_NORMAL标记,所以在使用D3DXComputeNormals之前必须克隆该网格并指定其包含D3DFVF_NORMAL标记的顶点格式。

if (!(pMesh->GetFVF() & D3DFVF_NORMAL))
{
	ID3DXMesh* pTempMesh = 0;
	pMesh->CloneMeshFVF(D3DXMESH_MANAGED, pMesh->GetFVF() | D3DFVF_NORMAL, Device, &pTempMesh);
	D3DXComputeNormals(pTempMesh, 0);
	pMesh->Release();
	pMesh = pTempMesh;
}

渐进网格

渐进网格用ID3DXPMesh接口来表示,它允许我们运用一系列的边折叠变换(ECT)对网格进行简化,每次ECT都移除一个顶点以及一个或俩个面,由于每次ECT都是可逆的,我们可对简化过程进行逆转从而将网格精确回复到初始状态,但意味着我们无法获得比原网格更丰富的细节。

与纹理中多级渐进纹理类似,如果对一个小而远的图元应用高分辨率的纹理实在是一种浪费,因为观察者不可能注意到这些细节,一个小而远的网格也完全不用像大而近的网格一样使用大量面片,所以在满足要求的条件下,总是用尽量少的面片来表达一个网格,以节省宝贵的绘制时间。

生成渐进网格

HRESULT D3DXGeneratePMesh(
    LPD3DXMESH pMesh, 
    CONST DWORD* pAdjacency, 
    CONST D3DXATTRIBUTEWEIGHTS *pVertexAttributeWeights,
    CONST FLOAT *pVertexWeights,
    DWORD MinValue, 
    DWORD Options, 
    LPD3DXPMESH* ppPMesh
);

pMesh:该输入变量包含了网格数据,渐进网格将根据此网格产生
pAdjacency:指向了包含了pMesh的邻接信息的DWORD类型的数组指针
pVertexAttributeWeights:指向D3DXATTRIBUTEWEIGHTS类型的结构数组的指针,数组维数为GetNumVertices(),数组的第i项对应于pMesh中第i个顶点,并指定了相应顶点的属性权值。顶点属性权值用于决定在简化过程中顶点被移除的概率,可为该参数传入NULL,则每个顶点将被赋予默认的属性权值。
pVertexWeights:指向一个float类型数组的指针,维数为GetNumVertices(),第i项对应于pMesh中的第i个顶点,并指定了相应顶点的属性权值。值越高简化过程中被移除的概率越小,可赋为NULL,这样每个顶点就会被赋为默认顶点权值1.0。
MinValue:网格中的顶点数或面片数可被简化到的下限,该值为一种期望值,实际还要依赖于顶点权值或属性权值,所以简化结果可能与该值不一致
Options:为D3DXMESHSIMP枚举成员
D3DXMESHSIMP_VERTEX  指定了前面的参数MinValue是指顶点数
D3DXMESHSIMP_FACE  指定了前面的参数MinValue是指面片数
ppPMesh:返回所生成的渐进网格

顶点属性权

该顶点权值结构允许我们为顶点每一个可能的分量指定一个权值,如果某个分量的权值被赋为0.0,则表明该分量无权值,顶点分量的权值越高在简化过程中该顶点被移除的概率越小。

typedef struct _D3DXATTRIBUTEWEIGHTS
{
    FLOAT Position;
    FLOAT Boundary;
    FLOAT Normal;
    FLOAT Diffuse;
    FLOAT Specular;
    FLOAT Texcoord[8];
    FLOAT Tangent;
    FLOAT Binormal;
} D3DXATTRIBUTEWEIGHTS, *LPD3DXATTRIBUTEWEIGHTS;

//各个分量默认权值,除非程序上有充分的理由,一般情况下建议使用这些默认值
D3DXATTRIBUTEWEIGHTS AttributeWeights;
AttributeWeights.Position = 1.0f;
AttributeWeights.Boundary = 1.0f;
AttributeWeights.Normal = 1.0f;	
AttributeWeights.Diffuse = 1.0f;
AttributeWeights.Specular = 1.0f;
AttributeWeights.Texcoord[8] = { 0.0f };

ID3DXPMesh其他接口

DWORD GetMaxFaces();		//返回渐进网格面片数可被指定的上限
DWORD GetMinFaces();		//返回渐进网格面片数可被指定的下限
DWORD GetMaxVertices();		//返回渐进网格顶点数可被指定的上限
DWORD GetMinVertices();		//返回渐进网格顶点数可被指定的下限

SetNumFaces设置网格面片数可被简化或细化到的个数,调整后的数量可能与期望的数量不一致,如果Faces小于GetMinFaces(),则将取为GetMinFaces,大于情况同理使用GetMaxFaces()

HRESULT SetNumFaces(DWORD Faces);	
//网格目前有50个面片,想将其简化为30
pMesh->SetNumFaces(30);

SetNumVertices设置网格顶点数可被简化或细化到的个数,同理会与期望顶点数不一致,过大过小会被限制到指定数量

HRESULT SetNumVertices(DWORD Vertices);

TrimByFaces允许重新设定面片最小值和最大值,俩个值必须位于[GetMinFaces(),GetMaxFaces()]内,同时也返回了重绘信息

HRESULT TrimByFaces(
	DWORD NewFacesMin, 
	DWORD NewFacesMax, 
	DWORD *rgiFaceRemap,	//面片重绘信息
	DWORD *rgiVertRemap		//顶点重绘信息
);

TrimByVertices允许重新设定顶点数的最小值和最大值,也是需要在区间[GetMinVerices(),GetMaxVerices()]内

HRESULT TrimByVertices(
	DWORD NewVerticesMin, 
	DWORD NewVerticesMax, 
	DWORD *rgiFaceRemap, 
	DWORD *rgiVertRemap
);

外接体

有时我们需要计算一个网格的外接体,俩种常见的外接体是外接球(bounding sphere)和外接体(bounding box),外接球或外接体常用于加速可见性检测碰撞检测。例如一个网格的外接体或外接球不可见,我们就可认为该网格不可见,检测外接体的可见性要比检测网格中每个面片的可见性的代价低得多。

假定场景中有一个发射物,要确定它是否会击中场景中某一个物体,由于物体是由三角形面片构成的,我们需要遍历每个物体的每个面片,并检测发射物(数学模型为射线)是否会击中某一面片。该方法需要进行大量的射线/三角形相交测试,场景中每个物体的每个三角形面片都要进行一次。一种更高效的途径是计算出每个网格(物体)的外接体,然后再对每个物体进行射线/外接体相交测试。如果射线与外接体相交则认为击中。如果希望提高精度,可借助外接体快速排除那些显然不能被击中的物体,然后再对那些极有可能被击中的物体使用更精确的方法来检测。所谓极有可能被击中的物体就是其外接体被击中的那些物体。

创建外接球D3DXComputeBoundingSphere

HRESULT WINAPI D3DXComputeBoundingSphere(
	CONST D3DXVECTOR3 *pFirstPosition,  // pointer to first position
	DWORD NumVertices,
	DWORD dwStride,                     // count in bytes to subsequent position vectors
	D3DXVECTOR3 *pCenter,
	FLOAT *pRadius
);

pFirstPosition:指向顶点数组中第一个顶点位置向量的指针
NumVertices:该顶点数组中顶点的个数
dwStride:每个顶点的大小,单位为字节,该值很重要,因为一种顶点结构可能包含了许多该函数所不需要的附加信息,如法向量和纹理坐标等,这样该函数就需要知道应跳过多少字节才能到下一个顶点的位置
pCenter:返回外接球的球心位置
pRedius:返回外接球的半径

创建外接体D3DXComputeBoundingBox

HRESULT WINAPI D3DXComputeBoundingBox(
    CONST D3DXVECTOR3 *pFirstPosition,  // pointer to first position
    DWORD NumVertices, 
    DWORD dwStride,                     // count in bytes to subsequent position vectors
    D3DXVECTOR3 *pMin, 
    D3DXVECTOR3 *pMax);

pMin:外接体的最小点
pMax:外接体的最大点

为了给外接体的使用提供便利,我们可为每种外接体实现一个类,将其放在d3d命名空间中

//外接体
struct BoundingBox
{
	BoundingBox();
	bool isPointInside(D3DXVECTOR3& p);
	D3DXVECTOR3 _min;
	D3DXVECTOR3 _max;
};

//外接球
struct BoundingSphere
{
	BoundingSphere();
	D3DXVECTOR3 _center;
	float _radius;
};


d3d::BoundingBox::BoundingBox()
{
	_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 d3d::BoundingBox::isPointInside(D3DXVECTOR3 & p)
{
	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;
	return false;
}

d3d::BoundingSphere::BoundingSphere()
{
	_radius = 0.0f;
}

特殊常量INFINITY、EPSILON

俩个十分有用的常量,将其添加到d3d命名空间中来方便使用,INFINITY表示float类型能存储的最大浮点数,可将改值概念化为无穷大,EPSILON是一个很小的值,如果某个小于该值,可认为该数为0。

const float ESPSILON = 0.001f;
const float INFINITY = FLT_MAX;

计算外接体例程

bool ComputeBoundingSphere(ID3DXMesh* mesh, d3d::BoundingSphere* 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,d3d::BoundingBox* 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;
}