DirectX的OBJ模型加载与渲染

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/zxx43/article/details/44109005

在之前的DirectX例子里我用的模型是.x文件,DirectX有一个方法D3DXLoadMeshFromX可以加载.x模型,但是这里有个问题,.x文件是没法用文本编辑器打开查看结构的,这里我来演示一下如何解析.obj模型.

首先让我们看一下.obj模型的组成部分以及结构,一个完整的obj模型一共分为三个部分:obj模型文件,mtl材质文件,纹理贴图;其中obj文件和mtl文件是可以用文本编辑器打开的,先打开obj文件,可以看到这样的内容:

v -3.000767 2.993211 2.014205
v -3.000767 -0.006789 2.014205
v -2.750767 2.993211 2.014205
v -2.750767 -0.006789 2.014205
v -2.750767 2.993211 2.014205
v -2.750767 -0.006789 2.014205
v -2.750767 2.993211 -1.985795
v -2.750767 -0.006789 -1.985795
v -2.750767 2.993211 -1.985795

vt 0.948633 0.500977
vt 0.948633 0.000977
vt 0.998633 0.500977
vt 0.998633 0.000977
vt 0.000000 0.500000
vt 0.000000 0.000000
vt 1.000000 0.500000
vt 1.000000 0.000000
vt 1.000000 0.501343
vt 0.000000 0.501343
vt 1.000000 0.438843

vn 0.000000 0.000000 1.000000
vn 1.000000 0.000000 0.000000
vn 0.000000 0.000000 -1.000000
vn -1.000000 0.000000 0.000000
vn 0.000000 1.000000 0.000000
vn 0.000000 -1.000000 0.000000
vn -0.000000 -0.707107 -0.707107
vn 0.000000 0.707107 0.707107
vn -0.000000 0.707107 -0.707106

g Left
usemtl wood
s 1
f 1/1/1 2/2/1 3/3/1
f 2/2/1 4/4/1 3/3/1
s 2
f 5/5/2 6/6/2 7/7/2
f 6/6/2 8/8/2 7/7/2
s 1
f 9/3/3 10/4/3 11/1/3
f 10/4/3 12/2/3 11/1/3
s 2
f 13/7/4 14/8/4 15/5/4
f 14/8/4 16/6/4 15/5/4
s 3
f 17/9/5 18/10/5 19/11/5
f 18/10/5 20/12/5 19/11/5
f 21/10/6 22/9/6 23/12/6
f 22/9/6 24/11/6 23/12/6

让我来解释一下字段:

首先v和其后三个值表示一个顶点的xyz坐标值;

vt和其后两个或者三个值表示顶点的纹理坐标uv(w);

vn和其后三个值表示顶点的法向量;

g表示一组面;

usemtl表示这个组用的mtl文件里那个材质的名称;

f及其后三组值表示一个面的三个 顶点/纹理/法线 在之前v,vt,vn集合里边的索引值.


打开mtl文件就会看到:

newmtl wood
illum 2
Kd 0.800000 0.800000 0.800000
Ka 0.200000 0.200000 0.200000
Ks 0.000000 0.000000 0.000000
Ke 0.000000 0.000000 0.000000
Ns 0.000000
map_Ka house/house.bmp
map_Kd house/house.bmp

newmtl后面的是材质名对应obj文件usemtl后面跟的值;

为了简单代码里只用到了map_Kd,它表示漫反射所使用的纹理名称;

其他的是光照属性,代码里采用默认材质的光照属性;


为了解析模型,首先要把材质文件给解析出来,把材质名称与纹理名称放入数组,这样解析obj的时候通过材质名称就能够找到对应材质数组的下标就能找到对应的纹理:

void MtlObj::getLineNum() {
	ifstream infile(path.c_str()); //打开指定文件
	string sline;//每一行
	while(getline(infile,sline)) {//从指定文件逐行读取
		if(sline[0]=='n'&&sline[1]=='e')//newmtl
			mtlNum++;
	}
	infile.close();
}

void MtlObj::readfile() {
	getLineNum();
	names=new string[mtlNum];
	textures=new string[mtlNum];
	int n=0;
	int t=0;

	ifstream infile(path.c_str()); //打开指定文件
	string sline;//每一行

	string value,name,texture;
	while(getline(infile,sline)) {//从指定文件逐行读取
		if(sline!="") {
			istringstream ins(sline);
			ins>>value;
			if(value=="newmtl") {
				ins>>name;
				names[n]=name;
				n++;
			} else if(value=="map_Kd") {
				ins>>texture;
				textures[t]=texture;
				t++;
			}
		}
	}
	infile.close();
}

int MtlObj::getIndexByName(string name) {
	int index=-1;
	for(int i=0;i<mtlNum;i++) {
		if(names[i]==name) {
			index=i;
			break;
		}
	}
	return index;
}

准备好了材质信息之后开始解析obj文件:

void ModelObj::getLineNum() {
	ifstream infile(path.c_str()); //打开指定文件
	string sline;//每一行

	while(getline(infile,sline)) {//从指定文件逐行读取
		if(sline[0]=='v') {
			if(sline[1]=='n')
				vnNum++;
			else if(sline[1]=='t')
				vtNum++;
			else
				vNum++;
		}
		if(sline[0]=='f')
			fNum++;
	}
	infile.close();

	ifstream ifile(path.c_str());
	string value,um,group,face;
	mtArr=new string[fNum];
	groupArr=new int[fNum];
	groupNum=0;
	int fi=0;
	while(getline(ifile,sline)) {
		istringstream ins(sline);
		ins>>value;
		if(value=="usemtl") {
			ins>>um;
			int mtlId=mtl->getIndexByName(um);
			groupMtlMap.insert(pair<int,int>(groupNum,mtlId));
		} else if(value=="g") {
			ins>>group;
			groupNum++;
		} else if(value=="f") {
			ins>>face;
			mtArr[fi]=um;
			groupArr[fi]=groupNum;
			fi++;
		}
	}
	ifile.close();
}
通过材质名字查找到该材质在之前的材质数组中的id,这边需要groupMtlMap保存面组id与材质id,那样在渲染时就可以通过面组id找到对应的纹理贴图.

取得基本信息后读入文件的详细内容:

void ModelObj::readfile() {
	getLineNum();
	vertices=new NormalTexVertex[fNum*3];
	indices=new int[fNum*3];

	//new二维数组
	vArr=new float*[vNum];
	for (int i=0;i<vNum;i++)
		vArr[i]=new float[3];

	vnArr=new float*[vnNum];
	for (int i=0;i<vnNum;i++)
		vnArr[i]=new float[3];

	vtArr=new float*[vtNum];
	for (int i=0;i<vtNum;i++)
		vtArr[i]=new float[3];

	fvArr=new int*[fNum];
	ftArr=new int*[fNum];
	fnArr=new int*[fNum];
	for (int i=0;i<fNum;i++) {
		fvArr[i]=new int[3];
		ftArr[i]=new int[3];
		fnArr[i]=new int[3];
	}
	ifstream infile(path.c_str());
	string sline;//每一行
	int ii=0,tt=0,jj=0,kk=0;

	std::string s1;
	float f2,f3,f4;

	while(getline(infile,sline)) {
		if(sline[0]=='v') {
			if(sline[1]=='n') {//vn
				istringstream ins(sline);
				ins>>s1>>f2>>f3>>f4;
				vnArr[ii][0]=f2;
				vnArr[ii][1]=f3;
				vnArr[ii][2]=f4;
				ii++;
			} else if(sline[1]=='t') {//vt
				istringstream ins(sline);
				ins>>s1>>f2>>f3>>f4;
				vtArr[tt][0]=f2;
				vtArr[tt][1]=1-f3;
				vtArr[tt][2]=f4;
				tt++;
			} else {//v
				istringstream ins(sline);
				ins>>s1>>f2>>f3>>f4;
				vArr[jj][0]=f2;
				vArr[jj][1]=f3;
				vArr[jj][2]=f4;
				jj++;
			}
		}
		if (sline[0]=='f') { //存储面
			istringstream in(sline);
			float a;
			in>>s1;//去掉f
			int i,k;
			for(i=0;i<3;i++) {
				in>>s1;
				//取出第一个顶点和法线索引
				a=0;
				for(k=0;s1[k]!='/';k++)
					a=a*10+(s1[k]-48);
				fvArr[kk][i]=a;

				a=0;
				for(k=k+1;s1[k]!='/';k++)
					a=a*10+(s1[k]-48);
				ftArr[kk][i]=a;

				a=0;
				for(k=k+1;s1[k];k++)
					a=a*10+(s1[k]-48);
				fnArr[kk][i]=a;
			}

			kk++;
		}
	}
	infile.close();
}
这里需要面数3倍的顶点,因为有n个三角形就有n*3个顶点,由于顶点之间可能不是共享法线和纹理坐标数据,因此不同三角形在同一个位置的顶点要分开来存放;

由于DirectX的纹理坐标轴v是朝下而模型的v坐标轴朝上,那么读取的纹理坐标v必须变成1-v才能有DirectX正确渲染,于是就有类似vtArr[tt][1]=1-f3这样的做法;

渲染的时候需要索引指针,大小是模型面的数量*3;


然后通过各种索引组装三角形:

void ModelObj::initTriangles() {
	for (int i=0;i<fNum;i++) {
		int v1Index=i*3;
		int v2Index=i*3+1;
		int v3Index=i*3+2;

		vertices[v1Index].x=vArr[fvArr[i][0]-1][0];
		vertices[v1Index].y=vArr[fvArr[i][0]-1][1];
		vertices[v1Index].z=vArr[fvArr[i][0]-1][2];
		vertices[v1Index].nx=vnArr[fnArr[i][0]-1][0];
		vertices[v1Index].ny=vnArr[fnArr[i][0]-1][1];
		vertices[v1Index].nz=vnArr[fnArr[i][0]-1][2];
		vertices[v1Index].u=vtArr[ftArr[i][0]-1][0];
		vertices[v1Index].v=vtArr[ftArr[i][0]-1][1];

		vertices[v2Index].x=vArr[fvArr[i][1]-1][0];
		vertices[v2Index].y=vArr[fvArr[i][1]-1][1];
		vertices[v2Index].z=vArr[fvArr[i][1]-1][2];
		vertices[v2Index].nx=vnArr[fnArr[i][1]-1][0];
		vertices[v2Index].ny=vnArr[fnArr[i][1]-1][1];
		vertices[v2Index].nz=vnArr[fnArr[i][1]-1][2];
		vertices[v2Index].u=vtArr[ftArr[i][1]-1][0];
		vertices[v2Index].v=vtArr[ftArr[i][1]-1][1];

		vertices[v3Index].x=vArr[fvArr[i][2]-1][0];
		vertices[v3Index].y=vArr[fvArr[i][2]-1][1];
		vertices[v3Index].z=vArr[fvArr[i][2]-1][2];
		vertices[v3Index].nx=vnArr[fnArr[i][2]-1][0];
		vertices[v3Index].ny=vnArr[fnArr[i][2]-1][1];
		vertices[v3Index].nz=vnArr[fnArr[i][2]-1][2];
		vertices[v3Index].u=vtArr[ftArr[i][2]-1][0];
		vertices[v3Index].v=vtArr[ftArr[i][2]-1][1];

		indices[i*3]=v1Index;
		indices[i*3+1]=v2Index;
		indices[i*3+2]=v3Index;
	}

	clearTriangles();
}

组装结束,清除读取文件用的内存:

void ModelObj::clearTriangles() {
	for(int i=0;i<vNum;i++)
		delete[] *(vArr+i);
	for(int i=0;i<vnNum;i++)
		delete[] *(vnArr+i);
	for(int i=0;i<vtNum;i++)
		delete[] *(vtArr+i);
	for(int i=0;i<fNum;i++) {
		delete[] *(fvArr+i);
		delete[] *(ftArr+i);
		delete[] *(fnArr+i);
	}

	delete[] vArr;
	delete[] vnArr;
	delete[] vtArr;
	delete[] fvArr;
	delete[] ftArr;
	delete[] fnArr;

	delete[] mtArr;
}

然后建立一个网格对象,把这些数据塞进去:

ObjModel::ObjModel(ModelObj* obj) {
	objLoader=obj;
	D3DXCreateMeshFVF(objLoader->fNum,objLoader->fNum*3,D3DXMESH_MANAGED,normalTexVertFvf,d3d,&mesh);
	initVertices();
	initTextures();

	DWORD* aAdjacency=new DWORD[objLoader->fNum*3];
	mesh->GenerateAdjacency(0.001,aAdjacency);
	mesh->OptimizeInplace(D3DXMESHOPT_ATTRSORT|D3DXMESHOPT_VERTEXCACHE,aAdjacency,NULL,NULL,NULL);
	delete[] aAdjacency;
}

void ObjModel::initVertices() {
	NormalTexVertex* vertices=NULL;
	mesh->LockVertexBuffer(0,(void**)&vertices);
	for(int i=0;i<objLoader->fNum*3;i++)
		vertices[i]=objLoader->vertices[i];
	mesh->UnlockVertexBuffer();

	WORD* indices=NULL;
	mesh->LockIndexBuffer(0,(void**)&indices);
	for(int i=0;i<objLoader->fNum*3;i++)
		indices[i]=objLoader->indices[i];
	mesh->UnlockIndexBuffer();

	DWORD* attributes=NULL;
	mesh->LockAttributeBuffer(0,&attributes);
	for(int i=0;i<objLoader->fNum;i++)
		attributes[i]=objLoader->groupArr[i]-1;
	mesh->UnlockAttributeBuffer();
}

接着通过之前的材质数组创建需要的纹理对象:
void ObjModel::initTextures() {
	objLoader->mtl->getLength(mtlNum);
	textures=new LPDIRECT3DTEXTURE9[mtlNum];
	for(int i=0;i<mtlNum;i++) {
		string texFile=TEX_PATH+objLoader->mtl->textures[i];
		D3DXCreateTextureFromFile(d3d,texFile.c_str(),&textures[i]);
	}
}

这下数据都准备好了,渲染吧:

void ObjModel::render() {
	for(DWORD i=0;i<(DWORD)objLoader->groupNum;i++) {
		map<int,int>::iterator itor=objLoader->groupMtlMap.find((int)(i+1));
		int mtlId=itor->second;
		d3d->SetTexture(1,textures[mtlId]);
		mesh->DrawSubset(i);
	}
}


最后创建对象并渲染之:

void initObjModel() {
	objLoader=new ModelObj(MODEL_TANK,MTL_TANK);
	objModel=new ObjModel(objLoader);
}
void renderObjModel() {
	objModel->render();
}
void releaseObjModel() {
	delete objModel;
	delete objLoader;
}

我导入了一个坦克模型,最终效果就像这样:




模型加载器的代码已经写好,下载地址: 点击下载


阅读更多
换一批

没有更多推荐了,返回首页