.obj loading code

BlitzMax Forums/MiniB3D Module/.obj loading code

JoshK(Posted 2007) [#1]
I just whipped this up in a few minutes. It handles quads and polygons. I did not bother with materials, and the way it is used here is slightly different from the LoadMesh() convention:
Function LoadObj:Int(file:String,surf:TSurface)
	Local stream:TStream
	Local position:TBank=New TBank
	Local normal:TBank=New TBank
	Local texcoords:TBank=New TBank
	Local line:String[]
	Local indices:String[]
	Local sumindices:Int
	Local x#,y#,z#,s$,a,b,c,na,nb,nc
	Local nx#,ny#,nz#,tu#,tv#,v,m#
	Local currentsmoothgroup
	Local face:TObjFace,vert:TObjVert,face2:TObjFace
	Local n,avg#,n2
	
	stream=ReadFile(file)
	If Not stream Return
	While Not stream.Eof()
		
		s$=Trim(stream.ReadLine())
		If s="" Continue
		If Left(s,1)="#" Continue
		
		line=s.split("")
		If line.length=0 Continue
		
		Select Lower(line[0])
			Case "v"
				If line.length<3 Return
				AppendFloat position,Float(line[1])
				AppendFloat position,Float(line[2])
				AppendFloat position,Float(line[3])
			
			Case "vt"
				If line.length<2 Return
				AppendFloat texcoords,Float(line[1])
				AppendFloat texcoords,-Float(line[2])
			
			Case "vn"
				If line.length<3 Return
				nx=Float(line[1])
				ny=Float(line[2])
				nz=Float(line[3])
				m=Sqr(nx*nx+ny*ny+nz*nz)
				nx:/m
				ny:/m
				nz:/m
				AppendFloat normal,nx
				AppendFloat normal,ny
				AppendFloat normal,nz
			
			Case "s"
				If line.length>1
					currentsmoothgroup=Int(line[1])
				EndIf
				
			Case "f"
				Local indice[line.length-1]
				
				Local verts:TObjVert[indice.length]
				face=New TObjFace
				face.verts=verts
				face.smoothgroup=currentsmoothgroup
				
				For n=0 To indice.length-1
					indices=line[n+1].split("/")
					tu=0
					tv=0
					nx=0
					ny=0
					nz=0
					
					vert=New TObjVert
					
					v=Int(indices[0])-1
					x#=position.PeekFloat(v*12+0)
					y#=position.PeekFloat(v*12+4)
					z#=position.PeekFloat(v*12+8)
					If indices.length>1
						v=Int(indices[1])-1
						tu#=texcoords.PeekFloat(v*8+0)
						tv#=texcoords.PeekFloat(v*8+4)
						If indices.length>2
							v=Int(indices[2])-1
							nx#=normal.PeekFloat(v*12+0)
							ny#=normal.PeekFloat(v*12+4)
							nz#=normal.PeekFloat(v*12+8)
							vert.normalsdefined=True
						EndIf
					EndIf
					
					vert.x=x
					vert.y=y
					vert.z=z
					vert.nx=nx
					vert.ny=ny
					vert.nz=nz
					vert.u=tu
					vert.v=tv
					face.verts[n]=vert
				Next
				
				'For Local n=2 To indice.length-1
				'	surf.addtriangle indice[n],indice[0],indice[n-1]
				'	sumindices:+3
				'Next
				
		EndSelect
	Wend
	
	'Calculate face normals
	For face=EachIn TObjFace.list
		If face.verts.length<3 Continue
		TriangleNormal(face.verts[0].x,face.verts[0].y,face.verts[0].z,face.verts[1].x,face.verts[1].y,face.verts[1].z,face.verts[2].x,face.verts[2].y,face.verts[2].z)
		face.nx=vector_x
		face.ny=vector_y
		face.nz=vector_z
		For n=0 To face.verts.length-1
			If Not face.verts[n].normalsdefined ' use hard edges for verts with undefined normal
				face.verts[n].nx=face.nx
				face.verts[n].ny=face.ny
				face.verts[n].nz=face.nz
			EndIf
		Next
	Next
	
	For face=EachIn TObjFace.list
		If face.smoothgroup
			For n=0 To face.verts.length-1
				nx#=face.nx
				ny#=face.ny
				nz#=face.nz
				avg#=1.0
				For face2=EachIn TObjFace.list' this part is highly inefficient, but don't give a fuck
					If face=face2 Continue
					If face.smoothgroup<>face2.smoothgroup Continue
					For n2=0 To face2.verts.length-1
						If Abs(face.verts[n].x-face2.verts[n2].x)>0.01 Continue
						If Abs(face.verts[n].y-face2.verts[n2].y)>0.01 Continue
						If Abs(face.verts[n].z-face2.verts[n2].z)>0.01 Continue
						nx:+face2.nx
						ny:+face2.ny
						nz:+face2.nz
						avg:+1
						n2=face2.verts.length-1
					Next
				Next
				face.verts[n].nx=nx/avg
				face.verts[n].ny=ny/avg
				face.verts[n].nz=nz/avg
			Next
		EndIf
	Next
	
	'Add triangles
	For face=EachIn TObjFace.list
		For n=0 To face.verts.length-1
			face.verts[n].index=surf.FindVertex(face.verts[n].x,face.verts[n].y,face.verts[n].z,face.verts[n].nx,face.verts[n].ny,face.verts[n].nz,face.verts[n].u,face.verts[n].v)
			If n>1
				surf.addtriangle face.verts[n].index,face.verts[0].index,face.verts[n-1].index
				sumindices:+3
			EndIf
		Next
	Next
	
	Return sumindices+1
EndFunction

Type TObjVert
	Field x#,y#,z#
	Field nx#,ny#,nz#
	Field u#,v#
	Field index
	Field normalsdefined
EndType

Type TObjFace
	Global list:TList=New TList
	
	Method New()
		list.addfirst(Self)
	EndMethod
	
	Field nx#,ny#,nz#
	Field verts:TObjVert[]
	Field smoothgroup
EndType

Global VECTOR_x#
Global VECTOR_y#
Global VECTOR_z#

Function TriangleNormal(Ax#,Ay#,Az#,Bx#,By#,Bz#,Cx#,Cy#,Cz#)
	Local ux#,uy#,uz#,vx#,vy#,vz#,vec:TVec4
	ux#=bx-ax
	uy#=by-ay
	uz#=bz-az
	vx#=cx-bx
	vy#=cy-by
	vz#=cz-bz
	VECTOR_x=uy#*vz#-uz#*vy#
	VECTOR_y=uz#*vx#-ux#*vz#
	VECTOR_z=ux#*vy#-uy#*vx#
	Local m#=Sqr(VECTOR_x*VECTOR_x+VECTOR_y*VECTOR_y+VECTOR_z*VECTOR_z)
	VECTOR_x:/m
	VECTOR_y:/m
	VECTOR_z:/m
EndFunction



klepto2(Posted 2007) [#2]
http://www.blitzbasic.com/codearcs/codearcs.php?code=2125

just for information ;) it handles materiallibs also ;)
Because your loader will not work as far as I see, because of some not integrated functions like Appendfloat .


JoshK(Posted 2007) [#3]
It's easy to figure out what AppendFloat() does.
And yours is about three times as much code.

I have found that a lot of modeling packages rely on smooth groups, and don't export correct normals data. I updated the code above to:

-Calculate hard edge normals if no normals data is present for any vertex
-Use the normals contained in the file
-Calculate normals based on face smooth groups in file


klepto2(Posted 2007) [#4]
How do you think that my code is 3 times as much?

Your code: 207 lines
My Code (Debuglogs deleted) : With Matlib 265, Without MatLib 205

thx for the code, but as you mentioned yours doesn't handle matlibs and isn't working like LoadMesh, mine does ;) .


JoshK(Posted 2007) [#5]
You could combine them to get proper normals handling, but I don't really care.


JoshK(Posted 2008) [#6]
This adds .mtl loading and support for relative vertex numbers and other weird stuff. It has been able to handle everything I have thrown at it:

CreateMeshLoader(LoadMeshObj,"obj")

Function LoadMeshObj:TEntity(path$)
	
	Local stream:TStream
	Local position:TBank=New TBank
	Local normal:TBank=New TBank
	Local texcoords:TBank=New TBank
	Local line:String[]
	Local indices:String[]
	Local sumindices:Int
	Local x#,y#,z#,s$,a,b,c,na,nb,nc
	Local nx#,ny#,nz#,tu#,tv#,v,m#
	Local currentsmoothgroup
	Local face:TObjFace,vert:TObjVert,face2:TObjFace
	Local n,avg#,n2
	Local currentgroup
	Local currentmaterialfile$
	Local materiallist:TObjMTL[]
	Local groupmaterial:TObjMTL[1024]
	
	stream=ReadFile(path)
	If Not stream Return

	Local mesh:TMesh=CreateMesh()
	
	While Not stream.Eof()
		
		s$=Trim(stream.ReadLine())
		If s="" Continue
		If Left(s,1)="#" Continue
		
		line=s.split("")
		If line.length=0 Continue
		
		Select Lower(line[0])
			Case "mtllib"
				If line.length>1
					materiallist=ParseMTLLib(ExtractDir(path)+"/"+line[1])
				EndIf
			
			Case "usemtl"
				currentgroup:+1
				If line.length>1
					For Local objmtl:TObjMTL=EachIn materiallist
						If objmtl.name.tolower()=line[1].tolower()
							groupmaterial[currentgroup]=objmtl
							Exit
						EndIf
					Next
				EndIf
				
				
			Case "v"
				If line.length<3 Return
				AppendFloat position,Float(line[1])
				AppendFloat position,Float(line[2])
				AppendFloat position,Float(line[3])
				
			Case "vt"
				If line.length<2 Return
				AppendFloat texcoords,Float(line[1])
				AppendFloat texcoords,-Float(line[2])
			
			Case "vn"
				If line.length<3 Return
				nx=Float(line[1])
				ny=Float(line[2])
				nz=Float(line[3])
				m=Sqr(nx*nx+ny*ny+nz*nz)
				nx:/m
				ny:/m
				nz:/m
				AppendFloat normal,nx
				AppendFloat normal,ny
				AppendFloat normal,nz
			
			Case "s"
				If line.length>1
					currentsmoothgroup=Int(line[1])
				EndIf
				
			Case "g"
				currentgroup:+1
							
			Case "f"
				Local indice[line.length-1]
				
				Local verts:TObjVert[indice.length]
				face=New TObjFace
				face.verts=verts
				face.group=currentgroup
				face.smoothgroup=currentsmoothgroup
				
				For n=0 To indice.length-1
					indices=line[n+1].split("/")
					tu=0
					tv=0
					nx=0
					ny=0
					nz=0
					
					vert=New TObjVert
					
					v=Int(indices[0])-1
					If v<0 v=position.size()/12+v+1
					
					x#=position.PeekFloat(v*12+0)
					y#=position.PeekFloat(v*12+4)
					z#=position.PeekFloat(v*12+8)
					If indices.length>1
						v=Int(indices[1])-1
						If v<0 v=texcoords.size()/8+v+1
						tu#=texcoords.PeekFloat(v*8+0)
						tv#=texcoords.PeekFloat(v*8+4)
						If indices.length>2
							v=Int(indices[2])-1
							If v<0 v=normal.size()/12+v+1
							nx#=normal.PeekFloat(v*12+0)
							ny#=normal.PeekFloat(v*12+4)
							nz#=normal.PeekFloat(v*12+8)
							vert.normalsdefined=True
						EndIf
					EndIf
					
					vert.x=x
					vert.y=y
					vert.z=z
					vert.nx=nx
					vert.ny=ny
					vert.nz=nz
					vert.u=tu
					vert.v=tv
					face.verts[n]=vert
				Next
				
				'For Local n=2 To indice.length-1
				'	surf.addtriangle indice[n],indice[0],indice[n-1]
				'	sumindices:+3
				'Next
				
		EndSelect
	Wend
	
	'Calculate face normals
	Local norm:TVec3
	For face=EachIn TObjFace.list
		If face.verts.length<3 Continue
		'TriangleNormal(face.verts[0].x,face.verts[0].y,face.verts[0].z,face.verts[1].x,face.verts[1].y,face.verts[1].z,face.verts[2].x,face.verts[2].y,face.verts[2].z)
		
		norm=TPlane.FromTriangle(vec3(face.verts[0].x,face.verts[0].y,face.verts[0].z),vec3(face.verts[1].x,face.verts[1].y,face.verts[1].z),vec3(face.verts[2].x,face.verts[2].y,face.verts[2].z)).normal()
		
		face.nx=-norm.x
		face.ny=-norm.y
		face.nz=-norm.z
		For n=0 To face.verts.length-1
			If Not face.verts[n].normalsdefined ' use hard edges for verts with undefined normal
				face.verts[n].nx=face.nx
				face.verts[n].ny=face.ny
				face.verts[n].nz=face.nz
			EndIf
		Next
	Next
	
	For face=EachIn TObjFace.list
		If face.smoothgroup
			For n=0 To face.verts.length-1
				nx#=face.nx
				ny#=face.ny
				nz#=face.nz
				avg#=1.0
				For face2=EachIn TObjFace.list' this part is highly inefficient, but don't give a fuck
					If face=face2 Continue
					If face.group<>face2.group Continue
					If face.smoothgroup<>face2.smoothgroup Continue
					For n2=0 To face2.verts.length-1
						If Abs(face.verts[n].x-face2.verts[n2].x)>0.01 Continue
						If Abs(face.verts[n].y-face2.verts[n2].y)>0.01 Continue
						If Abs(face.verts[n].z-face2.verts[n2].z)>0.01 Continue
						nx:+face2.nx
						ny:+face2.ny
						nz:+face2.nz
						avg:+1
						Exit
						'n2=face2.verts.length-1
					Next
				Next
				face.verts[n].nx=nx/avg
				face.verts[n].ny=ny/avg
				face.verts[n].nz=nz/avg
			Next
		EndIf
	Next
	
	Local badvertices=0
	
	'Add triangles
	Local submesh:TSubmesh
	For Local g=1 To currentgroup
		submesh=Null
		For face=EachIn TObjFace.list
			If face.group<>g Continue
			'Local s$=""
			If Not submesh
				submesh=CreateSubmesh(mesh)
				If groupmaterial[ g ]
					submesh.paint groupmaterial[ g ].material
					'Notify groupmaterial[ g ].material.name
				EndIf
			EndIf
			For n=0 To face.verts.length-1
				face.verts[n].index=submesh.surface.FindVertex(face.verts[n].x,face.verts[n].y,face.verts[n].z,face.verts[n].nx,face.verts[n].ny,face.verts[n].nz,face.verts[n].u,face.verts[n].v)
				's:+n+", "
				If n>1
					a=face.verts[n].index
					b=face.verts[n-1].index
					c=face.verts[0].index
					
					If (VertsMatchU(submesh.surface,a,b) And VertsMatchU(submesh.surface,a,c) And VertsMatchU(submesh.surface,b,c))=True Or (VertsMatchV(submesh.surface,a,b) And VertsMatchV(submesh.surface,a,c) And VertsMatchV(submesh.surface,b,c))=True
					'	Print "WARNING: Possible mesh error (identical texcoords in triangle)"
						badvertices:+1
					EndIf
					
					If a=b Or b=c Or a=c
						mesh.destroy()
						Return
					'	Notify "MESH ERROR"
					EndIf
					
					submesh.surface.addtriangle a,c,b
					sumindices:+3
				EndIf
			Next
		Next
		If submesh
			'If submesh.surface.counttriangles()>1024 submesh.surface.drawinstanced=0
		EndIf
	Next
	
	mesh.update()
	mesh.lock()
	
	TObjFace.list=New TList
	
	'Print badvertices+" bad triangles."
	
	Return mesh
	
	Function VertsMatchU%(surf:TSurface,a,b,e#=0.000001)
		If Abs(surf.vertexu(a)-surf.vertexu(b))>e Return False
		Return True
	EndFunction
	
	Function VertsMatchV%(surf:TSurface,a,b,e#=0.000001)
		If Abs(surf.vertexv(a)-surf.vertexv(b))>e Return False
		Return True
	EndFunction

	Function AppendFloat(bank:TBank,value#)
		Local size
		size=bank.size()
		bank.resize size+4
		bank.PokeFloat size,value
	EndFunction

EndFunction

Private

Type TObjVert
	Field x#,y#,z#
	Field nx#,ny#,nz#
	Field u#,v#
	Field index
	Field normalsdefined
EndType

Type TObjFace
	Global list:TList=New TList
	
	Method New()
		list.addfirst(Self)
	EndMethod
	
	Field nx#,ny#,nz#
	Field verts:TObjVert[]
	Field smoothgroup
	Field group
EndType

Type TObjMTL
	Field name:String
	'Field Brush:TBrush
	Field Texture:TTexture
	Field material:TMaterial
EndType

Function ParseMTLLib:TObjMTL[](Path:String)
	Local matStream:TStream = ReadStream(Path) 
	Local dir:String = ExtractDir(Path) + "/"
	If dir = "/" Then dir = ""
	If Not matStream Then Return Null
	Local MatLib:TObjMtl[0]
	Local CMI:Int = -1
	While Not Eof(matStream)
		Local Line:String = ReadLine(MatStream) 
		If Line[0..7] = "newmtl " Then
			MatLib = MatLib[..Matlib.Length + 1]
			CMI = MatLib.Length-1
			MatLib[CMI] = New TObjMtl
			MatLib[CMI].Name = Line[7..].Trim() 
			'MatLib[CMI].Brush = CreateBrush() 
			'BrushFX MatLib[CMI].Brush,4+16
			'DebugLog("Matname : " + Matlib[CMI].Name)
		EndIf
		'Colours
		If Line[0..3] = "Kd " Then
			Local Data:String = Line[3..].Trim()+" "
			Local F:Float[3]
			For Local I:Int = 0 To 2
				'Print "Before : " + Data
				Local FL:Int = Data.Find(" ")
				If I < 2 Then
					f[I] = Float(Data[..FL])
				Else
					f[i] = Float(Data) 
				EndIf
				Data = Data[FL+1..]
				'Print "After : " + Data
			Next
			'BrushColor(MatLib[CMI].Brush , F[0] * 255 , F[1] * 255 , F[2] * 255) 
			'DebugLog("MatColor : " +  (F[0] * 255) +","+(F[1] * 255)+","+(F[2] * 255))
		EndIf
		
		If Line[0..2] = "d " Then
			'BrushAlpha(MatLib[CMI].Brush , Float(Line[2..]))
			'DebugLog("MatAlpha : " + Float(Line[2..]) ) 
		EndIf
		
		If Line[0..3] = "Tr " Then
			'BrushAlpha(MatLib[CMI].Brush , Float(Line[2..])) 
			'DebugLog("MatAlpha : " + Float(Line[2..]) ) 
		EndIf 
		
		If Line[0..7] = "map_Kd " Then
			'MatLib[CMI].Texture = LoadTexture(dir+Line[7..].Trim())
			'Notify dir+Line[7..].Trim()
			'MatLib[CMI].Material = LoadMaterial(dir+Line[7..].Trim())
			'If Not MatLib[CMI].Material MatLib[CMI].Material = LoadMaterial(Line[7..].Trim())
			'If Not MatLib[CMI].Material 
			MatLib[CMI].Material = LoadMaterial( StripAll( Line[7..].Trim()) )
			'If MatLib[CMI].Texture <> Null BrushTexture(MatLib[CMI].Brush , MatLib[CMI].Texture) 
			'DebugLog("MatTexture : " + Line[7..].Trim() ) 
		EndIf
	Wend
	
	Return MatLib
End Function

Public