加载模型在我看来从来都不是一件容易的事情,尤其是我学习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个,第4个,第7个顶点构成,注意在写代码的时候注意,对应数组中的索引应该是0,3,6,因为OBJ中的索引好是从1开始的,而计算机语言种数组中是从0开始的,一般在赋值的时候会减1。
- 该面的顶点对应的纹理坐标是由纹理坐标数组种的第2个,第5个,第8个元素组成,注意在写代码的时候注意,对应数组中的索引应该是1,4,7,因为OBJ中的索引好是从1开始的,而计算机语言种数组中是从0开始的,一般在赋值的时候会减1。
- 该面的顶点对应的法线是由法线数组种的第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()读取同一个文件,内部只维护一个文件内部指针。