加载模型在我看来从来都不是一件容易的事情,尤其是我学习3D是从D3D入手的,模型加载直接一个函数就搞定了,对于内部细节从来都没有深入研究过。 .x文件用的比较少,所以对其理解也很肤浅。

      这本书使用的渲染API是OpenGL,对于习惯使用D3D的读者来书,这可能是件比较头大的事情。在读这本书之前,我稍微补了补OpenGL的基础,发现就以经完全够用了。所以D3D党完全不用担心,更何况懂点OpenGL也不是坏事,在学点OpenGL之后,我才明白原来的自己是多么的一叶障目,因为一种API就放弃一本书,太幼稚了。

1. OBJ格式解释浅析:

       言归正传,我们本节介绍的是OBJ文件模型加载,本节主要加载OBJ文件的4种内容,分别是顶点坐标,纹理坐标,法线以及面。

       这四种信息在OBJ文件中表示如下(以下内容只显示部分OBJ文件):

      #This is an obj 3d model file

       v -0.134214 1.696017 -0.152522   //这行是顶点

       v -0.128631 1.666882 -0.149329   //这行是顶点

 

       vn -0.456288 0.869882 0.138733   //这行是法线

       vn -0.460052 0.847785 0.244615   //这行是法线

 

       f 1//1 2//2 3//3                  //这行是三角形面

       f 3//3 4//4 1//1                  //这行是三角形面

       #end of file

       顶点和法线永远只有一种格式,如上。但是三角形面存在有好几种格式。表述面的行的通用格式一般为:

       f 1/2/3 4/5/6 7/8/9

       这行的意思就是:

  1. 该面由顶点数组中的第1个,第4个,第7个顶点构成,注意在写代码的时候注意,对应数组中的索引应该是0,3,6,因为OBJ中的索引好是从1开始的,而计算机语言种数组中是从0开始的,一般在赋值的时候会减1。
  2. 该面的顶点对应的纹理坐标是由纹理坐标数组种的第2个,第5个,第8个元素组成,注意在写代码的时候注意,对应数组中的索引应该是1,4,7,因为OBJ中的索引好是从1开始的,而计算机语言种数组中是从0开始的,一般在赋值的时候会减1。
  3. 该面的顶点对应的法线是由法线数组种的第3个,第6个,第9个元素组成,意在写代码的时候注意,对应数组中的索引应该是2,5,8,因为OBJ中的索引好是从1开始的,而计算机语言种数组中是从0开始的,一般在赋值的时候会减1。

        也就是说,对应三角形面行的含义如下:

              f                                1/2/3                                             4/5/6                                              7/8/9

        面描述符    顶点索引1/纹理坐标索引1/法线索引1     顶点索引2/纹理坐标索引2/法线索引2    顶点索引3/纹理坐标索引3/法线索引3

        这样,其他几种面的描述方式也就很容易理解了:

        f 1 2 3              //只有顶点索引

        f 1/4 2/5 3/6         //顶点索引+纹理坐标索引

        f 1/4/7 2/5/8 3/6/9    //顶点索引+纹理坐标索引+法线索引

        f 1//4 2//5 3//6       //顶点索引+法线索引

        OBJ格式解释就到这里。

2. OBJ格式加载:    

      OBJ加载类维护了4个向量,其中分别是点,纹理坐标,法线,面,其中点、纹理坐标、法线都是以buffer块的形式存储的,面中存储的是点,纹理坐标以及法线的索引,每次在绘制面的时候,跟面中所给的三个索引值,分别在buffer块中查找对应的点、纹理坐标以及法线值,然后传入OpenGL流水线,进行绘制,整个加载函数代码如下:

        加载思路:1.  每次加载一个字符,

                       2.  如果是该字符是'v',则该行可能是‘vt’,‘vn’也可能是‘v’需进一步判断。

          2.1  读入第二个字符;

          2.2  如果字符是‘t’,则读取纹理坐标;并设置包含纹理的bool变量为true;

                            2.2  如果字符是‘n’  则读取法线;并设置包含法线的bool变量为true;

                            2.3  如果字符是‘  ‘  则读取顶点;

                            2.4  如果不以上都不是,读取该行,并不做处理(认为是丢弃了);

                       3.  如果是'f'开头,则肯定是面数据行:

                            3.1  判断是否包含法线及纹理坐标:

                            3.2  均含,则读取纹理坐标索引 与 法线索引 + 顶点索引; 

                            3.3  含纹理索引不含法线索引则读取 纹理索引 + 顶点索引;

                            3.4  不含纹理索引含法线索引则读取 法线索引 + 顶点索引;

                            3.5  均不包含,则只读取顶点索引;

                        4.  返回1,继续循环,直到文件结束。

                        5.  讲维护的四个向量的首地址赋值给四个将要进行绘制用的数据指针(方便OpenGL调用)。

                        完。

void COBJ::Load(char * ModelName)
{
	FILE * fp;
	if(NULL == (fp = fopen(ModelName,"rt")))
	{
		cout<<"Open Obj Model File Failed"<<endl;
		return;
	}

	while (!feof(fp))
	{
		char LineBuffer[256];   //store every line of the obj file
		char iFirst, iNext;     //store the 1st and 2nd letter of the line

		float fTemp[3] = {0.0f, 0.0f, 0.0f};

		iFirst = fgetc(fp);
		if (iFirst == 'v')
		{
			iNext = fgetc(fp);
			if (iNext == ' '||iNext == '\t')    //test if the 2nd letter is space, if yes, then the line is vertex coordinate
			{
				fgets(LineBuffer, 256, fp);
				sscanf(LineBuffer, " %f %f %f", &fTemp[0], &fTemp[1], &fTemp[2]);
				m_VertexBuffer.push_back(fTemp);
			}

			else if (iNext == 't') //test if the 2nd letter is 't', if yes, then the texture coordinate
			{
				fgets(LineBuffer, 256, fp);
				sscanf(LineBuffer, " %f %f", &fTemp[0], &fTemp[1]);
				m_TexcoordBuffer.push_back(fTemp);
				isThereTexture = true;
			}

			else if (iNext == 'n') //test if the 2nd letter is 'n', if yes, then the normal
			{
				fgets(LineBuffer, 256, fp);
				sscanf(LineBuffer, " %f %f %f", &fTemp[0], &fTemp[1], &fTemp[2]);
				m_NormalBuffer.push_back(fTemp);
				isThereNormal = true;
			}
			else                   //we do not deal with such situation.
			{
				fgets(LineBuffer, 256, fp);
			}
		}
		else if (iFirst == 'f')
		{
			int temp[3][3];

			if(isThereTexture && isThereNormal)       //both texture and normal
			{
				fgets(LineBuffer, 256, fp);
				sscanf(LineBuffer, " %d/%d/%d %d/%d/%d %d/%d/%d",&temp[0][0],&temp[1][0],&temp[2][0],
				                                                 &temp[0][1],&temp[1][1],&temp[2][1],
																 &temp[0][2],&temp[1][2],&temp[2][2]);
			    m_FaceBuffer.push_back(&temp[0][0]);
			}
			else if (!isThereTexture && isThereNormal) //only normal
			{
				fgets(LineBuffer, 256, fp);
				sscanf(LineBuffer, " %d//%d %d//%d %d//%d",&temp[0][0],&temp[2][0],
														   &temp[0][1],&temp[2][1],
														   &temp[0][2],&temp[2][2]);
				m_FaceBuffer.push_back(&temp[0][0]);
			}
			else if ( isThereTexture && !isThereNormal) //only texture
			{
				fgets(LineBuffer, 256, fp);
				sscanf(LineBuffer, " %d/%d %d/%d %d/%d",&temp[0][0],&temp[1][0],
				                                        &temp[0][1],&temp[1][1],
				                                        &temp[0][2],&temp[1][2]);
				m_FaceBuffer.push_back(&temp[0][0]);
			}
			else                                       //neither texture nor normal, only vertex
			{
				fgets(LineBuffer, 256, fp);
				sscanf(LineBuffer, " %d %d %d",&temp[0][0],
											   &temp[0][1],
											   &temp[0][2]);
				m_FaceBuffer.push_back(&temp[0][0]);
			}
		}
		else
		{
			fgets(LineBuffer, 256, fp);
		}
	}
	m_pVertex   = &m_VertexBuffer[0];
	m_pTexcoord = &m_TexcoordBuffer[0];
	m_pNormal   = &m_NormalBuffer[0];
	m_pFace     = &m_FaceBuffer[0];
}

 3. 代码错误Q&A:

     3.1   重写的时候改了一下构成顶点,法线,纹理坐标的向量结构,使之既可以按照x,y,z访问向量子成员,也可以按照数组来访问子成员。但是出现了一个问题,就是打印顶点数据,xyz均相同,仔细看了下共用体代码,一下子就明白了:

struct VECTOR3
{
	union
	{
		float vec[3];
		struct  
		{
			float x,y,z;
		};
	};
	VECTOR3(float * pData)
	{
		memcpy(vec,pData,sizeof(float)*3);
	}
};

struct VECTOR2
{
	union
	{
		float vec[2];
		struct
		{
			float x,y;
		};
	};
	VECTOR2(float * pData)
	{
		memcpy(vec,pData,sizeof(float)*2);
	}
};

 当时写的时候,float x,y,z的外边把struct给掉了。

      3.2.   关于面数据行打印数据错误:最后传递给OBJ_FACE(int * pData)的参数是&fTemp[0][0],而非&fTemp[3][3],这个错误实在是太不应该了,直接导致数组越界,但是数组并不检查,所以打印出来的面数据行的索引非常奇怪。

      3.3.   读入和导出OBJ,注意OBJ格式的索引是从0开始,因此读入索引时,要减1;而导出索引时,要加1.

      3.4    同时使用fgets()和fgetc()读取同一个文件,内部只维护一个文件内部指针

 

 

posted on 2013-12-07 05:10  infinityward  阅读(578)  评论(0编辑  收藏  举报