.b3d Animations

BlitzMax Forums/BlitzMax Programming/.b3d Animations

AntonyWells(Posted 2006) [#1]
I'm doing a b3d loader for t2 and so far it loads static meshes perfectly but I'm about to do animations.
I can understand and load the data contained within the b3d, the series of keys and bones for each node but I'm not sure how to interpret this data.

Do I - for each vertex asscoiated with each bone - Transform each vert assigned to this bone through the bone's current rotation and postion.
Or do I first offset the position based on the node's current position.

I.e,

NewX = Node.X-VertexX
FinX = Node.LocalMatrix.Transform( NewX )
FinX = Node.X + FinX

Or simply

FinX = Node.LocalMatrix.Transform( VertexX )

It appears wierd to me because each node in a animated b3d has it's own relative position yet the mesh renderes fine with the vertex's default positions. Implying mark did something to the naked data to get it to be represented accurately by the file it's self.

Confusion.
Much.
confusion.


Chris C(Posted 2006) [#2]
is this some kind of forward compatability allowing offsets that most modelers dont allow for?

seems strange

If you've parsed out index and tri vertex info I wouldnt mind implementing b3d in my ode editor


AntonyWells(Posted 2006) [#3]
I'm not sure tbh. This is my first time implementing a skeleton based animation system so maybe it's the norm.
Where's mark when you need him.

[quote]If you've parsed out index and tri vertex info I wouldnt mind implementing b3d in my ode editor[/quote[

Here's the loader as is. Replace the calls to t2(I.e entity/vertex creation) with your own but it works fine for static entities.

Type TLoaderB3D Extends TLoader

	Method New()
		Name = "Blitz3D Mesh Loader Plugin"
		Author = "Antony Wells"
		Ver = "V1.0"
		Ext = "b3d"
	End Method

	Function Create:TLoaderB3D()
		Return New tloaderb3d
	End Function
	Field MediaPath:String
	Field Root:TEntity
	Field Stream:TStream
	Field MatList:TList,TexList:TList
	Method Load:TEntity(file:String)
	
		MediaPath = ExtractDir(file)
		Print "Media Path:"+MediaPath
		If mediapath="<bad_dir>" mediapath=""
		
		Root = Null
		Stream = ReadFile( File )
		If Stream = Null Throw "Unable to open B3D"
		MatList = CreateList()
		TexList = CreateList()
		
		Local Header:bChunk = Self.ReadChunk()
		If Header = Null Throw "Unable to read b3d header"
		
		If Header.Name<>"BB3D"	
			MainLog.Post "Not a valid b3d file "+File
			CloseFile stream
			Return Null
		Else
			MainLog.Post(file+" is a valid b3d file.")
		EndIf
		Local B3d_Version = ReadInt( Stream )
		If B3d_Version<>1 Throw "Trying to load b3d model version "+b3d_Version+" in v1.0 loader"
		
		ParseChunk( Header ) 
				
		
		CloseStream Stream

		Return Root
		
	End Method
	
	Method ReadChunk:bChunk()
		Local Out:bChunk = New bChunk
		For Local J=1 To 4
			Out.Name=Out.Name+Chr( ReadByte(Stream) )
		Next
		out.siz = ReadInt( Stream )
		out.begin = StreamPos( Stream )
		out.fin = out.begin+out.siz
		Return out
	End Method
	
	Method ParseChunk( From:BChunk )
	
		Local Cur:bChunk
		Local Mat:TMaterial
		Local Textures:Int
		Local NewEnt:Int
		If StreamPos(Stream) => StreamSize(Stream) Return
		Repeat
			
			Cur = ReadChunk()
			Select cur.name
				Case "BRUS" 'Material In Vivid Terms
					Print "Brush Reached."
					Local Textures = ReadInt( Stream )
					MainLog.Post("Textures Per Brush:"+Textures)
					
					
					While StreamPos( Stream ) < Cur.Fin
					    Local Mat:TMaterial = TMaterial.create()
						Local NullString:String = ReadText()
						Mat.Diffuse( ReadFloat( Stream ),ReadFloat( Stream ),ReadFloat( Stream ),ReadFloat( Stream ) )
						Mat.Shininess( ReadFloat( Stream ) )
						Mat.Name = NullString
						MainLog.Post "Material Called:"+NullString,0
						Local Blend:Int = ReadInt( Stream )
						Select Blend
							Case 1
							Mat.Blend( Blend_Normal )
							Case 2
							Mat.Blend( Blend_Add )
							Case 3 
							Mat.Blend( Blend_ADD )
							Default
							Throw "Unkonw blend mode>"+Blend
						End Select
										
						Local Fx:Int = ReadInt( Stream ) 
						If (fx & 1) = 1
							Mat.Ambient(255,255,255)
							Mat.Specular(255,255,255)
							Mat.Color(255,255,255)
						EndIf
						If (fx & 2) = 2
							Mat.ColorMode( 0)		
						Else
							Mat.ColorMode( 1)
						End If
						
						If (fx & 4) =4
							Mat.Shade( Shade_Flat )
						End If
						
						If (fx & 16) =16
							Mat.Cull( Cull_Off )
						Else
							Mat.Cull( Cull_Off )
						End If
											
						
					'	Main_Error.invoke "Has "+Textures+" Textures"
						For Local J:Int = 1 To Textures
							
							Local TextureNum = ReadInt( Stream )
							If TextureNum = -1
					'			Main_Error.invoke "Texture Slot "+J+" Empty"
							Else
								Local ThisTexture:TTexture =TTexture ( TexList.ValueAtIndex( TextureNum ) ) 
									
				'				Mat.AddTexture( ThisTexture )
								mat.AddTexture( ThisTexture)
							
							EndIf
																			
						Next
						
						ListAddLast MatList,Mat
						
					Wend
				

				Case "TEXS"
					Print "Texs"
				
					Local Texture:TTexture
					
					While StreamPos( Stream ) < Cur.Fin
					
						Local File:String = ReadText()
						Local Flags = ReadInt(Stream)
						Local Blend = ReadInt(Stream)
								
						If file<>""
							If (Flags & 64) = 64
								MainLog.Post "Sphere map ignored, not yet supported."
								'Texture = New TSphereMap
							Else
								Texture = New TTexture
							End If
							mainlog.post "Loading Texture : File:"+mediaPath+"/"+file
							'Texture.Load ( MediaPath+"/"+File )
							Texture = TTexture.Load( MediaPath+"/"+file )
							If texture = Null
								texture=TTexture.load( MediaPath+"/"+StripAll(file)+"."+ExtractExt(file) )
								If texture = Null
									texture = ttexture.load(file)
									If texture=Null
										texture = ttexture.load(StripAll(file)+"."+ExtractExt(file) )
										If texture=Null
											MainLog.Post("Could not resolve texture name "+file,False)
										EndIf
									EndIf
								EndIf
							EndIf
								
						'	Texture.ResPath = MediaPath+"\"
							
							Texture.Position( ReadFloat( Stream ),ReadFloat( Stream ),0)
							Texture.Scale( ReadFloat( Stream),ReadFloat( Stream ),0 )
							Texture.Rotate( 0,0,ReadFloat( Stream ) )
						
							If (Flags & 65537) = 65537
								Texture.CoordSet = 1
								MainLog.Post "Using Second Coord set",0
							End If
							
							mainLog.post "Tex blend>"+blend+" flag>"+flags
							Texture.ColorScale=1
							Select Blend 
								Case 0
									Texture.Blend( Texture_Normal )
								Case 1
									Texture.Blend( Texture_Normal )
								Case 2
									Texture.Blend( Texture_Modulated )
								Case 3
									Texture.Blend( Texture_Add )
								Case 4
									Texture.Blend( Texture_Dot3 )
								Case 5
									Texture.Blend( Texture_Modulated )
									Texture.ColorScale =2 
								Case 6
									Texture.Blend( Texture_Modulated )
									Texture.ColorScale= 4 
								Case 7
									Texture.Blend( Texture_Dot3Alpha )
							End Select
													
							ListAddLast TexList,Texture
					
						EndIf
					
					Wend
			
				Case "NODE"	
					Print "Node."
					Local W:Float,X:Float,Y:Float,Z:Float
					Local Name:String = ReadText()
					MainLog.Post "Entity Named>"+Name,0
					Local Entity:TEntity = New TEntity
					'Entity.Name = Name
					'main_error.invoke "Node>"+Entity.Name
					'Entity.IgnorePipeline=True
					Entity.Position( ReadFloat( Stream ),ReadFloat( Stream ),ReadFloat( Stream ) ,True )
					Entity.setScale( ReadFloat( Stream ),ReadFloat( Stream ),ReadFloat( Stream ) )
					aEnt = Entity
					
				'	Main_Error.invoke "X:"+entity.x+" Y:"+entity.y+" Z:"+entity.z,0
					If root = Null
						Root = Entity
					'	main_error.invoke "Was Root"
					Else
						If pStack[ pCount ] = Null
							' "Child entity without parent in b3d loader"
						EndIf					
						Entity.SetParent( pStack[ pCount ] )
					End If
					
									
					W = ReadFloat( Stream )
					X = ReadFloat( Stream )
					Y = ReadFloat( Stream )
					Z = ReadFloat( Stream )
					
					Entity.Quat = New Quaternion
					Entity.Quat.Set( X,Y,Z,W )
					
					
					Entity.RotateFromQuat()
					
					pCount:+1
					pStack[ pCount ] = Entity
					NewEnt = True
					ParseChunk( Cur )

				Case "MESH"
				
					aBrush = ReadInt( Stream )
					Print "Mesh reached."
					'main_Error.invoke "Mesh BrushID:"+aBrush
					ParseChunk( Cur )
				
				Case "VRTS"
					
					Print "Reading Verts"
					Local Flags:Int = ReadInt( Stream )
					Local Sets:Int = ReadInt( Stream )
					Local SetSize:Int = ReadInt( Stream )
				
					Local VertAt
					Local TmpCoord:Float[3]
					TmpSurf:TSurface = TSurface.Create()
					aEnt.vSurf = tmpSurf
					Local VertCIs
					
					While StreamPos( Stream ) < Cur.Fin
					
						VertAt = TmpSurf.AddVertex( ReadFloat(Stream),ReadFloat(Stream),ReadFloat(Stream) )
						VertCIs:+1
						If ( Flags & 1 ) = 1
							
							TmpSurf.VertexNormal( VertAt,ReadFloat( Stream),ReadFloat( stream),-ReadFloat(Stream) )
							
						End If
						
						If ( Flags & 2 ) = 2
						
							TmpSurf.VertexColor( VertAt, ReadFloat( Stream),ReadFloat(Stream),ReadFloat(Stream),ReadFloat(Stream) )
													
						End If
						
						For Local J = 0 To Sets-1
							
							For Local k=0 To SetSize-1
								TmpCoord[k] = ReadFloat( Stream )
							Next
							TmpSurf.VertexCoords(J,VertAt,TmpCoord[0],TmpCoord[1],TmpCoord[2] )
							
						Next
																			
					Wend 
			
			Case "TRIS"
					Print "Reading Tris."
					Local TMat:TMaterial
					Local MatId:Int = ReadInt( Stream )
					Local Mat:TMaterial,Surf:TSurface
					'Main_Error.Invoke "Loading Surface",0
					If MatId = -1
					'	Mat:TMaterial = New TMaterial
					Else
						mainLog.post "MatId>"+MatId
						 TMat = TMaterial( MatList.ValueAtIndex( MatId ) )
 						mainlog.post "Surface material:"+TMat.Name
 					End If
					Local triCIs:Int
					
					If tmpSurf = Null
					Throw "No surface previously defined in b3d loader"
					EndIf
					aSurf = TSurface.create()
					If aSurf = Null 
						Throw "Surface clone failed in b3d loader"
					EndIf
					aEnt.AddSurface( aSurf )
					
					aSurf.verts = tmpSurf.verts
					aSurf.tris = tmpSurf.tris
					aSurf.norms = tmpSurf.norms
					aSurf.cols = tmpSurf.cols
					asurf.coords = tmpSurf.coords
					aSurf.coordsets =tmpsurf.coordsets
					aSurf.VertC = tmpsurf.vertc
					If tMat<>Null
						aSurf.SetMaterial( TMat )
						mainLog.post "USing Material>"+TMat.Name
					EndIf
								
					While StreamPos( Stream )< Cur.Fin
						aSurf.AddTriangle( ReadInt( Stream ),ReadInt( Stream ),ReadInt( Stream ) )
					Wend
				'	main_error.invoke "Read "+tricIs+" tris",0
				    
				Case "ANIM"
				
					Local flags = ReadInt( stream )
					Local frames = ReadInt( stream )
					Local fps# = ReadFloat(stream)
					Print "Anim chunk. Flags:"+Flags+" Frames:"+Frames+" Fps:"+Fps	
				Case "BONE"
					
					aent.bone = New bone
					While StreamPos(Stream)<Cur.fin
						Int VertId = ReadInt()
						Int VertWeight = ReadFloat()
					Wend
				Case "KEYS"
					
					Local flag = ReadInt(stream)
					While StreamPos(stream)<Cur.fin
						Local frame:Int = ReadInt(Stream)
						Local x#,y#,z#
						Local rw#,rx#,ry#,rz#
						Local sx#,sy#,sz#
						If flag & 1
							x
						EndIf
						
					Wend														

			
			End Select

	'		Print Cur.NAme
			If NewEnt 
				pCount:-1
				NewEnt=False 
			EndIf

			SeekStream Stream,Cur.Fin
			
		Until StreamPos( Stream)=>From.Fin

	End Method
	Field pStack:TEntity[10000],pCount:Int
	Field aEnt:TEntity,aBrush
	Field TmpSurf:TSurface
	Field aSurf:TSurface
	Method ReadText:String()
	
	Local C:Byte,Text:String
	
		Repeat
			
			C = ReadByte( Stream )
			If C = 0 
				
				Return Text
			EndIf
			Text:+Chr( C )
			
		Forever
		
		Return ""
	
	End Method
	


End Type



Chris C(Posted 2006) [#4]
thanks for that! I'll have to modify it a bit to store vertex info in doubles for ode but I've put your code in my snippets folder for later use!


AntonyWells(Posted 2006) [#5]
No problem. You might wanna use floats instead of doubles when using gl btw. I went doubles mad at first but I found it really slowed things down on my gpu. Not sure if modern cards are optimized for that kinda percision though.

Back to my problem, i now have the animator code complete, and although the actual pivots(entities) joints are animating correctly(I added a box to each joint to visually see it) the actual mesh deformation is kind of whacky.

It works fine frame 1. Perfectly looks like psonic's freaky zombie should but as soon as I go up the frames it starts to deform badly.
Please if you're reading mark let me know where i'm going wrong. You're probably the only one with enough knowledge of the b3d format here.

Here's the code that creates the matrix to deform each bone by.

Method UpdateAnim()
		For Local s:tentity = EachIn subs
			s.calculateWorldMatrix(Null)
		Next
		TEntity.TmpSurf = RenSurf
		TEntity.Bas = VSurf
		For Local s:TEntity = EachIn subs
			s.TFormBones()
		Next
	End Method
	Global TmpSurf:TSurface
	Global Bas:TSurface
	Method CalculateWorldMatrix(Parent:TMatrix)
	
		Local Tmp:TMatrix = LocalRot.CreateCopy()
		Tmp.Multiply( LocalTrans )
		If Parent<>Null
			Tmp.Multiply( Parent )
		EndIf
		World = Tmp.CreateCopy()
				
		For Local s:Tentity = EachIn subs
			s.calculateWorldMatrix( tmp )
		Next
	
	End Method



I multiply each entities rotation and translation matrices by it's parent (Exactly in the same order as the entity is visually drawn)

Then I call the deformer here which is supposed to transform the base surfaces vert into the visual surface based on each bone.
IT does not work, is an understatement.

Method TFormBones()
		Local os:TSurface = TEntity.Bas
		Local rs:TSurface = TEntity.TmpSurf
		If Bone = Null 
		Return 
		EndIf
		For Local j=0 Until bone.vc
			Local Vert = bone.vertid[ j ]
			World.GetPosition()
			Local ox#,oy#,oz#
			ox = world.x()
			oy = world.y()
			oz = world.z()
			Local vx#,vy#,vz#
			vx = os.vertexX( vert )
			vy = os.vertexy( vert )
			vz = os.vertexz( vert )
			Local nx#,ny#,nz#
			nx = vx - ox
			ny = vy - oy
			nz = vz - oz
			
			world.tformvector( nx,ny,nz )
			nx = world.tformx
			ny = world.tformy
			nz = world.tformz
			vx = ox + nx
			vy = oy + ny
			vz = oz + nz
			rs.moveVertex( vert,vx,vy,vz )
			
		Next
		
		For Local t:Tentity = EachIn subs
			t.tformbones()
		Next
		
	End Method



Here's the code that computers the rotation postion based on the current anim time. IT slerps the rotation quat beween the left most and right most keyframes, and interpolates(I think that's the term) between the left and right anim frames position.
Scale is not yet implemeted because the model i'm using has no scale keys so it makes no difference.

Type TFrame
	Field Rot:Quaternion
	Field PosX#,posY#,posZ#
	Field scalX#,scaly#,scalz#
	Field Time#
	
	Function MakeFrame:TFrame( L:TFrame,R:TFrame,Scal# )
		
		Local out:TFrame = New tframe
		out.rot = New quaternion
		L.Rot.Slerp( out.rot,R.rot,scal )
		
		'out.rot = l.rot
		
		Local xd#,yd#,zd#
		xd = r.posx-l.posx
		yd = r.posy-l.posy
		zd = r.posz-l.posz
		out.posx = l.posx+xd*scal
		out.posy = l.posy+yd*scal
		out.posz = l.posz+zd*scal
		Return out
					
	End Function
	
End Type
Type TAnim
	Field Frame:TFrame[3000],frames
	
	Method FindLeft:TFrame( time# )
		
		For Local j=time To 1 Step -1
			If frame[j]<>Null
				Return frame[j]
			End If
		Next
			
	End Method
	Method FindRight:TFrame( time#,Ignore:TFrame)
		
		For Local j=time To frames
			If frame[j]<>Null And frame[j]<>ignore
				Return frame[j]
			EndIf
		Next
		
	End Method
	
End Type

Method SetAnimTime( time# )
		AnimTime = time
		Print "SetAnimTime Called."
		If animC <>Null
			
			time = Time Mod AnimC.frames
			If time<1 time =1
			If time>animc.frames
				mainlog.post("Mod failed.",True)
			EndIf
			
			For Local s:tentity = EachIn subs
				s.SetFramer( time )
			Next
						
		Else
			MainLog.Post("Entity has no aasscoiated animation tracks.",True)
		EndIf
	
	End Method
	
	Method SetFramer(Time#)
		If NoAnim = True Return 
		If animC<>Null
			mainlog.post("Cannot directly animate top-tier entity.",True)
		End If
		
		Local LeftFrame:TFrame = Anim.FindLeft( Time )
		Local RightFrame:TFrame = Anim.FindRight( Time,LeftFrame )
		If leftFrame = RightFrame
			cframe.rot = leftFrame.rot
			cframe.posx = leftframe.posx
			cframe.posy = leftframe.posy
			cframe.posz = leftframe.posz
		Else
			
			Local dis# = RightFrame.time-LeftFrame.time
			Local ltime# = time-leftframe.time
			Local scal# = ltime/dis
			cframe = TFrame.MakeFrame( LeftFrame,RightFrame,Scal )
			cframe.rot.toAngles()
			
		EndIf
		
		localrot.rotate( cframe.rot.pitch,cframe.rot.yaw,cframe.rot.roll )
		localtrans.position( cframe.posx,cframe.posy,cframe.posz )
				
		For Local s:tentity = EachIn subs
			s.setframer( time )
		Next
													
	End Method