我很久以前就想在我的引擎中实现这个功能(为obj文件添加纹理),而你的问题让我有了实际去做它的心情:)。
你提供的图像看起来更像是预览而不是纹理。此外,如您在预览中所见,纹理坐标与其不对应。
![3D preview](https://istack.dev59.com/aOY7o.webp)
如果您查看纹理坐标:
vt 0.736102 0.263898
vt 0.263898 0.736102
vt 0.263898 0.263898
vt 0.736102 0.263898
vt 0.263898 0.736102
vt 0.263898 0.263898
vt 0.736102 0.263898
vt 0.263898 0.736102
vt 0.263898 0.263898
vt 0.736102 0.263898
vt 0.263898 0.736102
vt 0.263898 0.263898
vt 0.736102 0.263898
vt 0.263898 0.736102
vt 0.263898 0.263898
vt 0.736102 0.736102
vt 0.736102 0.736102
vt 0.736102 0.736102
vt 0.736102 0.736102
vt 0.736102 0.736102
这里只有2个数字:
0.736102
0.263898
这对于纹理中的轴对齐的四边形或正方形子图像是有意义的,如果它不存在于您的纹理中。此外,纹理点的数量 20
没有意义,应该只有 4
。因此您所遇到的困惑。
无论如何,Rabbid76 是正确的,您需要复制点……这相对容易,所以:
extract all positions, colors, texture points and normals
from your obj file into separate tables. So parse lines starting with v,vt,vn
and create 4 tables from it. Yes 4 as color is sometimes encoded in v
as v x y z r g b
as output from some 3D scanners.
So you should have something like this:
double ppos[]=
{
-1.000000, 1.000000, 1.000000,
-1.000000,-1.000000,-1.000000,
-1.000000,-1.000000, 1.000000,
-1.000000, 1.000000,-1.000000,
1.000000,-1.000000,-1.000000,
1.000000, 1.000000,-1.000000,
1.000000,-1.000000, 1.000000,
1.000000, 1.000000, 1.000000,
};
double pcol[]=
{
};
double ptxr[]=
{
0.736102,0.263898,
0.263898,0.736102,
0.263898,0.263898,
0.736102,0.263898,
0.263898,0.736102,
0.263898,0.263898,
0.736102,0.263898,
0.263898,0.736102,
0.263898,0.263898,
0.736102,0.263898,
0.263898,0.736102,
0.263898,0.263898,
0.736102,0.263898,
0.263898,0.736102,
0.263898,0.263898,
0.736102,0.736102,
0.736102,0.736102,
0.736102,0.736102,
0.736102,0.736102,
0.736102,0.736102,
};
double pnor[]=
{
-0.5774, 0.5774, 0.5774,
-0.5774,-0.5774,-0.5774,
-0.5774,-0.5774, 0.5774,
-0.5774, 0.5774,-0.5774,
0.5774,-0.5774,-0.5774,
0.5774, 0.5774,-0.5774,
0.5774,-0.5774, 0.5774,
0.5774, 0.5774, 0.5774,
};
process faces f
now you should handle the above tables as temp data and create real data for your mesh from scratch into new structure (or load it directly to VBOs). So what you need is to reindex all the f
data to unique combinations of all the indexes present. To do that you need to keep track what you already got. For that I amusing this structure:
class vertex
{
public:
int pos,txr,nor;
vertex(){}; vertex(vertex& a){ *this=a; }; ~vertex(){}; vertex* operator = (const vertex *a) { *this=*a; return this; };
int operator == (vertex &a) { return (pos==a.pos)&&(txr==a.txr)&&(nor==a.nor); }
int operator != (vertex &a) { return (pos!=a.pos)||(txr!=a.txr)||(nor!=a.nor); }
};
so create empty list of vertex
now process first f
line and extract indexes
f 1/1/1 2/2/2 3/3/3
so for each point (process just one at a time) in the face extract its ppos,ptxr,pnor
index. Now check if it is already present in your final mesh data. If yes use its index instead. If not add new point to all tables your mesh have (pos,col,txr,nor
) and use index of newly added point.
When all points of a face was processed add the face with the reindexed indexes into your final mesh faces and process next f
line.
这是我在我的引擎中使用的Wavefront OBJ加载器C++类(但它依赖于引擎本身,因此您不能直接使用它,它只用于查看代码结构和如何编码...从头开始可能会很困难)。
#ifndef _model_obj_h
#define _model_obj_h
class model_obj
{
public:
class vertex
{
public:
int pos,txr,nor;
vertex(){}; vertex(vertex& a){ *this=a; }; ~vertex(){}; vertex* operator = (const vertex *a) { *this=*a; return this; };
int operator == (vertex &a) { return (pos==a.pos)&&(txr==a.txr)&&(nor==a.nor); }
int operator != (vertex &a) { return (pos!=a.pos)||(txr!=a.txr)||(nor!=a.nor); }
};
OpenGL_VAO obj;
model_obj();
~model_obj();
void reset();
void load(AnsiString name);
int save(OpenGL_VAOs &vaos);
};
model_obj::model_obj()
{
reset();
}
model_obj::~model_obj()
{
reset();
}
void model_obj::reset()
{
obj.reset();
}
void model_obj::load(AnsiString name)
{
int adr,siz,hnd;
BYTE *dat;
reset();
siz=0;
hnd=FileOpen(name,fmOpenRead);
if (hnd<0) return;
siz=FileSeek(hnd,0,2);
FileSeek(hnd,0,0);
dat=new BYTE[siz];
if (dat==NULL) { FileClose(hnd); return; }
FileRead(hnd,dat,siz);
FileClose(hnd);
AnsiString s,s0,t;
int a,i,j;
double alpha=1.0;
List<double> f;
List<int> pos,txr,nor;
List<double> ppos,pcol,pnor,ptxr;
vertex v;
List<vertex> pv;
f.allocate(6);
ppos.num=0;
pcol.num=0;
pnor.num=0;
ptxr.num=0;
obj.reset();
obj.addVBO(_OpenGL_VBO_purpose_pos ,vbo_loc_pos , GL_ARRAY_BUFFER,GL_FLOAT, 3, 0.0001);
obj.addVBO(_OpenGL_VBO_purpose_col ,vbo_loc_col , GL_ARRAY_BUFFER,GL_FLOAT, 4, 0.0001);
obj.addVBO(_OpenGL_VBO_purpose_txr0,vbo_loc_txr0, GL_ARRAY_BUFFER,GL_FLOAT, 2, 0.0001);
obj.addVBO(_OpenGL_VBO_purpose_nor ,vbo_loc_nor , GL_ARRAY_BUFFER,GL_FLOAT, 3, 0.0001);
obj.addVBO(_OpenGL_VBO_purpose_fac , -1,GL_ELEMENT_ARRAY_BUFFER, GL_INT, 3, 0.0);
obj.draw_mode=GL_TRIANGLES;
obj.rep.reset();
obj.filename=name;
_progress_init(siz); int progress_cnt=0;
for (adr=0;adr<siz;)
{
progress_cnt++; if (progress_cnt>=1024) { progress_cnt=0; _progress(adr); }
s0=txt_load_lin(dat,siz,adr,true);
a=1; s=str_load_str(s0,a,true);
f.num=0; for (i=0;i<6;i++) f.dat[i]=0.0;
if (s=="v")
{
f.num=0;
for (;;)
{
s=str_load_str(s0,a,true);
if ((s=="")||(!str_is_num(s))) break;
f.add(str2num(s));
}
if (f.num>=3)
{
ppos.add(f[0]);
ppos.add(f[1]);
ppos.add(f[2]);
}
if (f.num==6)
{
pcol.add(f[3]);
pcol.add(f[4]);
pcol.add(f[5]);
}
}
else if (s=="vn")
{
f.num=0;
for (;;)
{
s=str_load_str(s0,a,true);
if ((s=="")||(!str_is_num(s))) break;
f.add(str2num(s));
}
pnor.add(f[0]);
pnor.add(f[1]);
pnor.add(f[2]);
}
else if (s=="vt")
{
f.num=0;
for (;;)
{
s=str_load_str(s0,a,true);
if ((s=="")||(!str_is_num(s))) break;
f.add(str2num(s));
}
ptxr.add(f[0]);
ptxr.add(f[1]);
}
else if (s=="f")
{
pos.num=0;
txr.num=0;
nor.num=0;
for (;;)
{
s=str_load_str(s0,a,true); if (s=="") break;
for (t="",i=1;i<=s.Length();i++) if (s[i]=='/') break; else t+=s[i]; if ((t!="")&&(str_is_num(t))) pos.add(str2int(t)-1);
for (t="",i++;i<=s.Length();i++) if (s[i]=='/') break; else t+=s[i]; if ((t!="")&&(str_is_num(t))) txr.add(str2int(t)-1);
for (t="",i++;i<=s.Length();i++) if (s[i]=='/') break; else t+=s[i]; if ((t!="")&&(str_is_num(t))) nor.add(str2int(t)-1);
}
for (i=0;i<pos.num;i++)
{
v.pos=pos[i];
if (txr.num>0) v.txr=txr[i]; else v.txr=-1;
if (nor.num>0) v.nor=nor[i]; else v.nor=-1;
for (j=0;j<pv.num;j++)
if (v==pv[j])
{ pos[i]=j; j=-1; break; }
if (j>=0)
{
j=v.pos; j=j+j+j; if (pcol.num>0) obj.addpntcol(ppos[j+0],ppos[j+1],ppos[j+2],pcol[j+0],pcol[j+1],pcol[j+2],alpha);
else obj.addpnt (ppos[j+0],ppos[j+1],ppos[j+2]);
j=v.nor; j=j+j+j; if (v.nor>=0) obj.addnor (pnor[j+0],pnor[j+1],pnor[j+2]);
j=v.txr; j=j+j; if (v.txr>=0) obj.addtxr (ptxr[j+0],ptxr[j+1]);
pos[i]=pv.num; pv.add(v);
}
}
for (i=2;i<pos.num;i++) obj.addface(pos[0],pos[i-1],pos[i]);
}
}
_progress_done();
delete[] dat;
}
int model_obj::save(OpenGL_VAOs &vaos)
{
int vaoix0=-1;
OpenGL_VBO *vn=obj.getVBO(_OpenGL_VBO_purpose_nor );
if (vn->data.num==0) obj.nor_compute();
vaos.vao=obj;
vaoix0=vaos.add(obj);
return vaoix0;
}
#endif
它目前尚未使用 *.mtl 文件(我在预览中硬编码了纹理)。请注意,如果我将此作为纹理使用:
![texture](https://istack.dev59.com/NVfQY.webp)
结果如下所示:
![preview](https://istack.dev59.com/bDeh4.webp)
我在这里使用了很多自己的东西,因此需要解释一下:
str_load_str(s,i,true)
返回表示字符串 s
中从索引 i
开始的第一个有效单词的字符串。true 表示更新 i
为 s
中的新位置。
str_load_lin(s,i,true)
返回表示字符串 s
中从索引 i
开始的一行(直到 CR
或 LF
或 CRLF
或 LFCR
)。true 表示更新 i
为该行后面的新位置。
txt_load_...
与上述类似,但它是从 BYTE*
或 CHAR*
中读取而不是从字符串中读取。
请注意,AnsiString
的索引从 1
开始,而 BYTE*,CHAR*
的索引从 0
开始。
我还使用了自己的动态列表模板,所以:
List<double> xxx;
与double xxx[];
相同
xxx.add(5);
将5
添加到列表末尾
xxx[7]
访问数组元素(安全)
xxx.dat[7]
访问数组元素(不安全但直接访问速度快)
xxx.num
是数组的实际使用大小
xxx.reset()
清除数组并设置xxx.num=0
xxx.allocate(100)
为100
个项目预分配空间
这里是更新后的更快的重新索引代码,其中包含来自mtl文件的纹理(其他内容被忽略,现在仅支持单个对象/纹理):
#ifndef _model_obj_h
#define _model_obj_h
class model_obj
{
public:
class vertex
{
public:
int pos,txr,nor;
vertex(){}; vertex(vertex& a){ *this=a; }; ~vertex(){}; vertex* operator = (const vertex *a) { *this=*a; return this; };
int operator == (vertex &a) { return (pos==a.pos)&&(txr==a.txr)&&(nor==a.nor); }
int operator != (vertex &a) { return (pos!=a.pos)||(txr!=a.txr)||(nor!=a.nor); }
int operator < (vertex &a)
{
if (pos>a.pos) return 0;
if (pos<a.pos) return 1;
if (txr>a.txr) return 0;
if (txr<a.txr) return 1;
if (nor<a.nor) return 1;
return 0;
}
void ld(int p,int t,int n) { pos=p; txr=t; nor=n; }
};
class vertexes
{
public:
List<vertex> pv;
List<int> ix;
int m;
vertexes(){}; vertexes(vertexes& a){ *this=a; }; ~vertexes(){}; vertexes* operator = (const vertexes *a) { *this=*a; return this; };
void reset() { m=0; pv.num=0; ix.num=0; }
bool get(int &idx,vertex &v)
{
int i,j;
if (ix.num<=0)
{
m=1;
idx=0;
pv.add(v);
ix.add(0);
return true;
}
for (j=0,i=m;i;i>>=1)
{
j|=i;
if (j>=ix.num) { j^=i; continue; }
if (v<pv.dat[ix.dat[j]]) j^=i;
}
idx=ix.dat[j];
if (v==pv.dat[idx]) return false;
idx=pv.num; pv.add(v); j++;
if (j>=ix.num) ix.add(idx);
else ix.ins(j,idx);
if (ix.num>=m+m) m<<=1;
return true;
}
};
struct material
{
AnsiString nam,txr;
material(){}; material(material& a){ *this=a; }; ~material(){}; material* operator = (const material *a) { *this=*a; return this; };
};
List<material> mat;
OpenGL_VAO obj;
model_obj();
~model_obj();
void reset();
void load(AnsiString name);
int save(OpenGL_VAOs &vaos);
};
model_obj::model_obj()
{
reset();
}
model_obj::~model_obj()
{
reset();
}
void model_obj::reset()
{
obj.reset();
mat.reset();
}
void model_obj::load(AnsiString name)
{
AnsiString path=ExtractFilePath(name);
int adr,siz,hnd;
BYTE *dat;
reset();
siz=0;
hnd=FileOpen(name,fmOpenRead);
if (hnd<0) return;
siz=FileSeek(hnd,0,2);
FileSeek(hnd,0,0);
dat=new BYTE[siz];
if (dat==NULL) { FileClose(hnd); return; }
FileRead(hnd,dat,siz);
FileClose(hnd);
AnsiString s,s0,t;
int a,i,j;
double alpha=1.0;
List<double> f;
List<int> pos,txr,nor;
List<double> ppos,pcol,pnor,ptxr;
vertex v;
vertexes pver;
material m0,*m=NULL;
f.allocate(6);
pver.reset();
ppos.num=0;
pcol.num=0;
pnor.num=0;
ptxr.num=0;
obj.reset();
obj.addVBO(_OpenGL_VBO_purpose_pos ,vbo_loc_pos , GL_ARRAY_BUFFER,GL_FLOAT, 3, 0.0001);
obj.addVBO(_OpenGL_VBO_purpose_col ,vbo_loc_col , GL_ARRAY_BUFFER,GL_FLOAT, 4, 0.0001);
obj.addVBO(_OpenGL_VBO_purpose_txr0,vbo_loc_txr0, GL_ARRAY_BUFFER,GL_FLOAT, 2, 0.0001);
obj.addVBO(_OpenGL_VBO_purpose_nor ,vbo_loc_nor , GL_ARRAY_BUFFER,GL_FLOAT, 3, 0.0001);
obj.addVBO(_OpenGL_VBO_purpose_fac , -1,GL_ELEMENT_ARRAY_BUFFER, GL_INT, 3, 0.0);
obj.draw_mode=GL_TRIANGLES;
obj.rep.reset();
obj.filename=name;
_progress_init(siz); int progress_cnt=0;
for (adr=0;adr<siz;)
{
progress_cnt++; if (progress_cnt>=1024) { progress_cnt=0; _progress(adr); }
s0=txt_load_lin(dat,siz,adr,true);
a=1; s=str_load_str(s0,a,true);
f.num=0; for (i=0;i<6;i++) f.dat[i]=0.0;
if (s=="v")
{
f.num=0;
for (;;)
{
s=str_load_str(s0,a,true);
if ((s=="")||(!str_is_num(s))) break;
f.add(str2num(s));
}
if (f.num>=3)
{
ppos.add(f[0]);
ppos.add(f[1]);
ppos.add(f[2]);
}
if (f.num==6)
{
pcol.add(f[3]);
pcol.add(f[4]);
pcol.add(f[5]);
}
}
else if (s=="vn")
{
f.num=0;
for (;;)
{
s=str_load_str(s0,a,true);
if ((s=="")||(!str_is_num(s))) break;
f.add(str2num(s));
}
pnor.add(f[0]);
pnor.add(f[1]);
pnor.add(f[2]);
}
else if (s=="vt")
{
f.num=0;
for (;;)
{
s=str_load_str(s0,a,true);
if ((s=="")||(!str_is_num(s))) break;
f.add(str2num(s));
}
ptxr.add(f[0]);
ptxr.add(f[1]);
}
else if (s=="f")
{
pos.num=0;
txr.num=0;
nor.num=0;
for (;;)
{
s=str_load_str(s0,a,true); if (s=="") break;
for (t="",i=1;i<=s.Length();i++) if (s[i]=='/') break; else t+=s[i]; if ((t!="")&&(str_is_num(t))) pos.add(str2int(t)-1);
for (t="",i++;i<=s.Length();i++) if (s[i]=='/') break; else t+=s[i]; if ((t!="")&&(str_is_num(t))) txr.add(str2int(t)-1);
for (t="",i++;i<=s.Length();i++) if (s[i]=='/') break; else t+=s[i]; if ((t!="")&&(str_is_num(t))) nor.add(str2int(t)-1);
}
for (i=0;i<pos.num;i++)
{
v.pos=pos[i];
if (txr.num>0) v.txr=txr[i]; else v.txr=-1;
if (nor.num>0) v.nor=nor[i]; else v.nor=-1;
if (pver.get(pos[i],v))
{
j=v.pos; j=j+j+j; if (pcol.num>0) obj.addpntcol(ppos[j+0],ppos[j+1],ppos[j+2],pcol[j+0],pcol[j+1],pcol[j+2],alpha);
else obj.addpnt (ppos[j+0],ppos[j+1],ppos[j+2]);
j=v.nor; j=j+j+j; if (v.nor>=0) obj.addnor (pnor[j+0],pnor[j+1],pnor[j+2]);
j=v.txr; j=j+j; if (v.txr>=0) obj.addtxr (ptxr[j+0],ptxr[j+1]);
}
}
for (i=2;i<pos.num;i++) obj.addface(pos[0],pos[i-1],pos[i]);
}
else if (s=="mtllib")
{
AnsiString s1;
int adr,siz,hnd;
BYTE *dat;
s=str_load_str(s0,a,true);
s+=str_load_lin(s0,a,true);
siz=0;
hnd=FileOpen(path+s,fmOpenRead);
if (hnd<0) continue;
siz=FileSeek(hnd,0,2);
FileSeek(hnd,0,0);
dat=new BYTE[siz];
if (dat==NULL) { FileClose(hnd); continue; }
FileRead(hnd,dat,siz);
FileClose(hnd);
m=&m0;
for (adr=0;adr<siz;)
{
s1=txt_load_lin(dat,siz,adr,true);
a=1; s=str_load_str(s1,a,true);
if (s=="newmtl")
{
s=str_load_str(s1,a,true);
s+=str_load_lin(s1,a,true);
mat.add();
m=&mat[mat.num-1];
m->nam=s;
m->txr="";
}
else if (s=="map_Kd")
{
s=str_load_str(s1,a,true);
s+=str_load_lin(s1,a,true);
m->txr=s;
}
}
delete[] dat;
m=NULL;
}
else if (s=="usemtl")
{
s=str_load_str(s0,a,true);
s+=str_load_lin(s0,a,true);
for (m=mat.dat,i=0;i<mat.num;i++,m++)
if (m->nam==s) { i=-1; break; }
if (i>=0) m=NULL;
}
}
for (i=0;i<mat.num;i++)
if (mat[i].txr!="")
{
OpenGL_VAO::_TXR txr;
txr.ix=-1;
txr.unit=txr_unit_map;
txr.filename=mat[i].txr;
txr.txrtype=GL_TEXTURE_2D;
txr.repeat=GL_REPEAT;
obj.txr.add(txr);
}
_progress_done();
delete[] dat;
}
int model_obj::save(OpenGL_VAOs &vaos)
{
int vaoix0=-1,i;
OpenGL_VBO *vn=obj.getVBO(_OpenGL_VBO_purpose_nor );
if (vn) if (vn->data.num==0) obj.nor_compute();
vaos.vao=obj;
vaoix0=vaos.add(obj);
return vaoix0;
}
#endif
除了添加的材质(目前只有纹理和材质名称),我还改变了重新索引的方式,使顶点按索引排序,并使用二分查找来按需获取顶点索引。使用这种方法,100K faces Standford dragon (3.4MByte) 可以在3.7秒内加载:
![dragon](https://istack.dev59.com/ty64j.webp)
vertexIndices
,textureIndices
和normalIndices
。你必须将它们合并为一个索引集,该集合引用长度相等的三个数组,包括顶点坐标、纹理坐标和法向量。 - Rabbid76