前言
最近处理一些网格渲染的时候,需要解析Obj文件,从Free3D上随便找了个免费的人体obj模型解析测试一波
国际惯例,参考博客:
本文所使用的从Free3D下载的模型[1]
.obj文件格式与.mtl文件格式[2]
详解3D中的obj文件格式[3]
3D中OBJ文件格式解析[4]
obj文件解析[5]
维基百科Wavefront.objfile[6]
ObjectFiles(.obj)[7]
MTLFilesMaterialDefinitionsforOBJFiles[8]
python读取obj[9]
obj的可视化软件MeshLab[10]
文件结构
【注】各种obj文件可能有所不同,但是包含的字段的意义都是一样的,对号入座理解其意即可,如果有笔记中没有描述的字段,说明不太常用,自行查阅相关文献。
以下载到简单格式obj为例:
#用于注释
mtllib
mtllib:代表材质库,通常指向到某个mtl文件
#3dsMaxWavefrontOBJExporterv0.97b-(c)2007guruware
#作成したファイル:05.11.201915:52:38
newmtl091_W_Aya_2K_01
Ns30.0000
Ni1.5000
d1.0000
Tr0.0000
Tf1.00001.00001.0000
illum2
Ka0.55000.55000.5500
Kd1.00001.00001.0000
Ks0.00000.00000.0000
Ke0.00000.00000.0000
map_Katex 91_W_Aya_2K_01.jpg
map_Kdtex 91_W_Aya_2K_01.jpg
这个文件的重点在于:map_ka和map_kd分别表示贴图路径
其它字段
接下来就是obj里面通常包含的:
v(vertices):几何形状的顶点,因为物体是由面构成的,而面是由线构成的,线由点构成的,所以无论是何形状,都必须要有几何顶点;一些应用支持顶点颜色,通过在xyz后面跟上red,green,blue值来表示。颜色值的范围为0到1.0。
#Listofgeometricvertices,with(x,y,z[,w])coordinates,wisoptionalanddefaultsto1.0.
v0.1230.2340.3451.0
v…
vt(vertextexture):顶点纹理,代表当前顶点对应纹理图的哪个像素,通常是0-1,然后根据纹理图的宽高去计算具体像素位置
#Listoftexturecoordinates,in(u,[,v,w])coordinates,thesewillvarybetween0and1.v,wareoptionalanddefaultto0.
vt0.5001[0]
vt…
vn(vertexnormal):顶点法线,物理里面有说过眼睛看到物体是因为光线经过物体表面反射到眼睛,所以这个法线就是通过入射光线计算反射光线使用的法线。
#Listofvertexnormalsin(x,y,z)form;normalsmightnotbeunitvectors.
vn0.7070.0000.707
vn…
f(face):大部分几何体都包括面,除非是像头发丝那一类模型只包含一根根头发的顶点,而且大部分模型的头发也用的面片的方法渲染的。常见的面是由3个顶点构成的三角面,当然也有多边形的,不过我见得少。其格式有几种形式:
仅包含顶点fv1v2v3….包含顶点和纹理fv1/vt1v2/vt2v3/vt3…包含顶点纹理和法线fv1/vt1/vn1v2/vt2/vn2v3/vt3/vn3…包括顶点和法线fv1//vn1v2//vn2v3//vn3…
o、g、s、usemtl
o091_W_Aya_100K_01
g091_W_Aya_100K_01
usemtl091_W_Aya_2K_01
soff
其中:
usemtl为模型指定一个材质o:代表object,表示不同的对象名称g:代表group,顶点或者三角面片的集合名称s:代表smooth
不同形式的记录方法都是使用斜杠区分不同的字段索引,f后面跟的是正整数,代表对应字段的索引。
#Polygonalfaceelement(seebelow)
f12
3f3/14/25/
3f6/4/13/5/37/6/5
f7//18//29//
3f…
比较容易理解,每个顶点都是代表空间中的一个位置,这个位置可以给他带个颜色,而且这个位置也可以有法线。
【注】其实除了顶点发现,有些非obj的模型文件还定义了面法线,比如VRay渲染软件。
代码显示
之前学PRNet的人脸重建和这个非常类似,也是读到顶点v的三维坐标以后,将二维纹理图通过vt映射到每个顶点,通过面着色,一个面有三个顶点,通过这三个顶点就能计算出当前面的颜色。
按照读文件的方法逐行读取obj,然后通过正则表达式找到v、vt、vn、f
filename=’091_W_Aya_10K.obj’
vertices=[]
vertex_norm=[]
vertex_tex=[]
triangles=[]
texcoords=[]
forlineinopen(filename,”r”):
values=line.split()
if(values==[]):
continue
if(values==’#’):
continue
if(values[0]==’v’):
vertices.append([float(values[1]),float(values[2]),float(values[3])])
if(values[0]==’vn’):
vertex_norm.append([float(values[1]),float(values[2]),float(values[3])])
if(values[0]==’vt’):
vertex_tex.append([float(values[1]),float(values[2]),float(values[3])])
if(values[0]==’f’):
face=[]
texcoord=[]
norm=[]
forvinvalues[1:]:
w=v.split(‘/’)
face.append(int(w[0]))
if(len(w)>=2andlen(w[1])>0):
texcoord.append(int(w[1]))
else:
texcoord.append(-1)
if(len(w)>=3andlen(w[2])>0):
norm.append(int(w[2]))
else:
norm.append(-1)
triangles.append(face)
texcoords.append(texcoord)
因为索引都是从1开始,而python等语言的数组通常从0开始,所以需要将索引-1
vertices=np.array(vertices,np.int32)
vertices=vertices-np.min(vertices)
vertices[…,1]=np.max(vertices[…,1])-vertices[…,1]
triangles=np.array(triangles,np.int)-1
texcoords=np.array(texcoords,np.int)-1
vertex_tex=np.array(vertex_tex,np.float32)
接下来纹理图:
uv_map=plt.imread(“./tex/091_W_Aya_2K_01.jpg”)
uv_map=uv_map.astype(‘float32’)
uv_map=np.rot90(uv_map,3)
最后一行旋转图片的原因是,plt读取的图像像素坐标和obj存储的坐标顺序有这样的一个关系。
最终,使用opencv画三角面,每个三角面的颜色为三个顶点对应颜色的平均值
img_3D=np.zeros((np.max(vertices[…,1]),np.max(vertices[…,0]),3),dtype=np.uint8)
foriinrange(triangles.shape[0]):
cnt=np.array([(vertices[triangles[i,0],0],vertices[triangles[i,0],1]),
(vertices[triangles[i,1],0],vertices[triangles[i,1],1]),
(vertices[triangles[i,2],0],vertices[triangles[i,2],1])],dtype=np.int32)
color_coord1_x=int(vertex_tex[texcoords[i,0],0]*uv_map.shape[0])
color_coord1_y=int(vertex_tex[texcoords[i,0],1]*uv_map.shape[1])
color_coord2_x=int(vertex_tex[texcoords[i,1],0]*uv_map.shape[0])
color_coord2_y=int(vertex_tex[texcoords[i,1],1]*uv_map.shape[1])
color_coord3_x=int(vertex_tex[texcoords[i,2],0]*uv_map.shape[0])
color_coord3_y=int(vertex_tex[texcoords[i,2],1]*uv_map.shape[1])
color=(uv_map[color_coord1_x,color_coord1_y,…]+
uv_map[color_coord2_x,color_coord2_y,…]+
uv_map[color_coord3_x,color_coord3_y,…])/3.0
img_3D=cv2.drawContours(img_3D,[cnt],0,(int(color[0]),int(color[1]),int(color[2])),-1)
plt.figure(figsize=(16,16))
plt.imshow(img_3D/255.0)
plt.axis(‘off’)
对比一下与meshlab可视化结果
为什么会出现这种情况,感觉有些纹理不对,原因在于我们做opencv可视化的时候,并没有区分先渲染前面还是先渲染后面,而是按照obj存储的三角面顺序着色,所以导致了图里面有些背面覆盖了前面,出现这种一块一块的不对劲效果,其实异常块是模型不可见部分的颜色而已。有兴趣可自行优化一下渲染算法,区分一下背面和正面。
后记
分析一下obj格式的3D模型文件的各个字段原理,同时进行简单的可视化测试
完整的python脚本实现放在微信公众号的简介中描述的github中,有兴趣可以去找找,同时文章也同步到微信公众号中,有疑问或者兴趣欢迎公众号私信。
参考资料
[1]
本文所使用的从Free3D下载的模型:https://free3d.com/zh/3d-model/091_aya-3dsmax-2020-189298.html
[2]
.obj文件格式与.mtl文件格式:https://blog.csdn.net/u013467442/article/details/46792495
[3]
详解3D中的obj文件格式:https://www.jianshu.com/p/f7f3e7b6ebf5
[4]
3D中OBJ文件格式解析:https://52zju.cn/?p=186887
[5]
obj文件解析:https://www.cnblogs.com/3dgiser/articles/10157353.html
[6]
维基百科Wavefront.objfile:https://en.wikipedia.org/wiki/Wavefront_.obj_file
[7]
ObjectFiles(.obj):http://paulbourke.net/dataformats/obj/
[8]
MTLFilesMaterialDefinitionsforOBJFiles:https://people.sc.fsu.edu/~jburkardt/data/mtl/mtl.html
[9]
python读取obj:https://gist.github.com/tshirtman/8648d954d70d46e58a7c
[10]
obj的可视化软件MeshLab:https://www.meshlab.net/