Code archives/3D Graphics - Mesh/Vertex animation

This code has been declared by its author to be Public Domain code.

Download source code

Vertex animation by Yasha2009
EDIT 2: If anyone's still watching this - DLL version is available upon request that is (at least on my system) as fast as B3D animation (but not faster).

Not sure how useful this actually is...

This basically uses native Blitz commands to play MD2-style vertex morph animations (loaded from custom "MDX" files, but with a little imagination you could easily make it read real MD2s). I had hoped it would provide all the advantages of Blitz3D meshes while retaining the speed of MD2 animation. Sadly that doesn't seem to be the case - since each mesh needs to be a separate surface, most of the same speed hit from using B3Ds is still there. Even so it is usable for smaller numbers of characters. Apart from the speed, it is worth pointing out that since this moves the vertices every frame with VertexCoords, it is possible to cast shadows onto meshes animated in this way. Animations also don't suffer from the limitations of MD2, although if you've converted them from an MD2 file this is obviously irrelevant.

EDIT: If ported to miniB3D this system is in fact significantly faster than B3D animation.

See the comments for a couple of short programs to convert MD2 and B3D files to MDX format.

This demo was designed with the dragon.md2 mesh from the dragon demo in Samples\mak. You can convert it with the MD2 to MDX converter.
Const MAXFRAMES=100
Const MAXVERTS=1000
Const MAXSEQS=10

Global appheight=768
Global appwidth=1024
Global appdepth=32

AppTitle "Vertex morph animation";,"Are you sure you want to quit?"

Type MDX
	Field mesh,surf,verts			;Mesh, surface and texture handles; Total no. vertices
	Field tex,texname$				;Texture handle, and filename. Add more slots if using more than one texture
	Field cframe#,frames			;Current frame, total no. frames
	Field fr.TFrame[MAXFRAMES]		;All frames
	Field cseq,seqs					;Current anim sequence, total no. sequences
	Field seq.sequence[MAXSEQS]		;Animation sequences
	Field speed#,mode,dir			;Animation speed, mode (0=stop,1=loop,2=ping-pong,3=one-shot), direction (mode 2)
	Field tlen,tt					;Transition length and time (for tweening between sequences)
End Type

Type TFrame
	Field vx#[MAXVERTS],vy#[MAXVERTS],vz#[MAXVERTS]		;Position
	Field nx#[MAXVERTS],ny#[MAXVERTS],nz#[MAXVERTS]		;Normal
End Type

Type sequence
	Field start		;First frame
	Field finish	;Last frame
	Field speed#	;Default play speed
End Type


SC_FPS=60	;Desired framerate
rtime=Floor(1000.0/SC_FPS)
limited=True

Graphics3D appwidth,appheight,appdepth,6
SetBuffer BackBuffer()

centrecam=CreatePivot()
PositionEntity centrecam,0,15,0
camera=CreateCamera(centrecam)
PositionEntity camera,0,20,-50,1

sun=CreateLight()
PositionEntity sun,-100,400,0
PointEntity sun,centrecam

ground=CreateMesh()
parquet=CreateSurface(ground)
v1=AddVertex(parquet,-125,0,150):v2=AddVertex(parquet,125,0,150):v3=AddVertex(parquet,125,0,-100)
AddTriangle(parquet,v1,v2,v3):v2=AddVertex(parquet,-125,0,-100):AddTriangle(parquet,v1,v3,v2)
EntityColor ground,0,0,255
block=CreateCube():ScaleMesh block,20,5,20


dragon.MDX=LoadMDX("dragon.mdx"):d=dragon\mesh:PositionEntity d,0,17,0:ScaleMDXMesh(dragon,0.5,0.5,0.5)
dragon\texname="dragon.bmp":dragon\tex=LoadTexture(dragon\texname):EntityTexture d,dragon\tex


While Not KeyDown(1)
	ctime=MilliSecs()

	MoveEntity camera,0,KeyDown(200)-KeyDown(208),KeyDown(30)-KeyDown(44)
	TurnEntity centrecam,0,KeyDown(203)-KeyDown(205),0
	MoveEntity centrecam,0,(KeyDown(31)-KeyDown(45))*0.2,0
	PointEntity camera,centrecam
	
	If KeyHit(57) Then AnimateMDX(dragon.MDX,1,0.1,0,0)
	If KeyHit(28) Then AnimateMDX(dragon.MDX,1,0.1,0,20)	;Tween!
	If KeyHit(38) Then limited=Not limited					;Turn off frame limit
	
	If MilliSecs()-render_time=>1000 Then fps=frames:frames=0:render_time=MilliSecs():Else frames=frames+1
	
	UpdateMDX
	UpdateWorld
	RenderWorld
	
	Text 0,30,"FPS: "+fps
	Text 0,60,"Current frame: "+dragon\cframe
	
	n=rtime-(MilliSecs()-ctime)		;Free spare CPU time
	Delay n-(limited+1)
	
	Flip limited
Wend

End


Function LoadMDX.MDX(fname$)	;Load an MDX file
	meshbank=CreateBank(FileSize(fname))
	filein=ReadFile(fname)
	ReadBytes(meshbank,filein,0,BankSize(meshbank))
	CloseFile filein
	
	scale#=PeekFloat(meshbank,0)
	sfac#=PeekFloat(meshbank,4)
	
	ent.MDX=New MDX
	ent\mesh=CreateMesh():s=CreateSurface(ent\mesh):ent\surf=s
	
	ent\frames=PeekShort(meshbank,8)	;Number of frames
	fsize=PeekInt(meshbank,10)			;Size of a frame in bytes
	ent\verts=PeekShort(meshbank,14)	;No. verts
	ent\seqs=PeekShort(meshbank,28)		;No. anim sequences
	
	For v=0 To ent\verts-1		;Store the UVs, don't bother with other data yet
		vu#=PeekShort(meshbank,34+v*4)/65535.0
		vv#=PeekShort(meshbank,36+v*4)/65535.0
		AddVertex(s,0,0,0,vu,vv)
	Next
	
	np=PeekInt(meshbank,16)
	ts=PeekShort(meshbank,np)		;No. tris
	For t=0 To ts-1		;Triangle info
		v0=PeekShort(meshbank,np+2+t*6)
		v1=PeekShort(meshbank,np+4+t*6)
		v2=PeekShort(meshbank,np+6+t*6)
		AddTriangle(s,v0,v1,v2)
	Next
	
	np=PeekInt(meshbank,20):nlen=0		;Texture data offset
	texnum=PeekByte(meshbank,np)		;Number of textures
	For t=0 To texnum-1
		pos=PeekInt(meshbank,np+1+t*4)
		namelen=PeekShort(meshbank,pos)
		ent\texname=""					;Add more texture slots if intending to use more than one texture
		For v=0 To namelen-1
			ent\texname=ent\texname+Chr(PeekByte(meshbank,pos+2+v))
		Next
		If FileType(ent\texname)=1 Then ent\tex=LoadTexture(ent\texname):EntityTexture ent\mesh,ent\tex
	Next
	
	np=PeekInt(meshbank,24)
	For f=0 To ent\frames-1		;Frame data
		ent\fr[f]=New TFrame
		For v=0 To ent\verts-1
			pos=np+(f*ent\verts+v)*9
			ent\fr[f]\vx[v]=PeekSShort(meshbank,pos)/sfac
			ent\fr[f]\vy[v]=PeekSShort(meshbank,pos+2)/sfac
			ent\fr[f]\vz[v]=PeekSShort(meshbank,pos+4)/sfac
			ent\fr[f]\nx[v]=PeekSByte(meshbank,pos+6)/127.0
			ent\fr[f]\ny[v]=PeekSByte(meshbank,pos+7)/127.0
			ent\fr[f]\nz[v]=PeekSByte(meshbank,pos+8)/127.0
		Next
	Next
	ent\fr[MAXFRAMES]=New TFrame		;Temporary frame (for transition tweening)
	
	np=PeekInt(meshbank,30)
	For sq=0 To ent\seqs-1
		ent\seq[sq]=New sequence
		ent\seq[sq]\start=PeekShort(meshbank,np+sq*8)
		ent\seq[sq]\finish=PeekShort(meshbank,np+sq*8+2)
		ent\seq[sq]\speed=PeekFloat(meshbank,np+sq*8+4)
	Next
	
	For v=0 To ent\verts-1		;Set up the mesh at frame 0 (entirely optional)
		VertexCoords s,v,ent\fr[0]\vx[v],ent\fr[0]\vy[v],ent\fr[0]\vz[v]
		VertexNormal s,v,ent\fr[0]\nx[v],ent\fr[0]\ny[v],ent\fr[0]\nz[v]
	Next
	
	FreeBank meshbank
	Return ent
End Function

Function SaveMDX(ent.MDX,fname$)	;Save an MDX to file after changing it
	meshbank=CreateBank(34)
	numframes=ent\frames
	
	SetMDXFrame(ent,0)
	scale#=MaxRadius(ent\mesh):sfac#=32765.0/scale	;Prevent rounding errors that might result in changing sign
	PokeFloat meshbank,0,scale:PokeFloat meshbank,4,sfac
	
	s=ent\surf							;Only one surface for now
	vs=ent\verts
	PokeShort meshbank,8,numframes		;Number of frames
	PokeInt meshbank,10,vs*9			;Size of a frame in bytes
	PokeShort meshbank,28,ent\seqs		;Number of anim sequences (only one, no way to get sequence data from an b3d. Change it later if necessary)
	
	PokeShort meshbank,14,vs:ResizeBank meshbank,34+(vs*4)+2
	
	For v=0 To vs-1		;Store the UVs, don't bother with other data yet
		PokeShort meshbank,34+v*4,VertexU(s,v)*65535
		PokeShort meshbank,36+v*4,VertexV(s,v)*65535
	Next
	
	ts=CountTriangles(s):np=BankSize(meshbank)
	PokeShort meshbank,np-2,ts:ResizeBank meshbank,np+ts*6
	PokeInt meshbank,16,np-2	;Offset for triangle data
	For t=0 To ts-1		;Triangle data
		PokeShort meshbank,np+t*6,TriangleVertex(s,t,0)
		PokeShort meshbank,np+2+t*6,TriangleVertex(s,t,1)
		PokeShort meshbank,np+4+t*6,TriangleVertex(s,t,2)
	Next
	FreeEntity mesh
	
	pos=BankSize(meshbank):PokeInt meshbank,20,pos	;Offset for texture name
	texnum=1	;Need to make a couple of changes to the system to manage more textures
	ResizeBank meshbank,pos+1+texnum*4
	PokeByte(meshbank,pos,texnum)
	For t=0 To texnum-1
		np=BankSize(meshbank):PokeInt meshbank,pos+1+t*4,np
		ResizeBank meshbank,np+2+Len(ent\texname):PokeShort(meshbank,np,Len(ent\texname))
		For v=1 To Len(ent\texname)
			PokeByte(meshbank,np+1+v,Asc(Mid(ent\texname,v,1)))
		Next
	Next
	
	np=BankSize(meshbank):PokeInt meshbank,24,np	;Offset for frame data
	ResizeBank meshbank,np+ent\frames*vs*9
	
	For f=0 To numframes-1		;Frame data
		For v=0 To vs-1
			pos=np+(f*vs+v)*9
			PokeSShort meshbank,pos,ent\fr[f]\vx[v]*sfac
			PokeSShort meshbank,pos+2,ent\fr[f]\vy[v]*sfac
			PokeSShort meshbank,pos+4,ent\fr[f]\vz[v]*sfac
			PokeSByte meshbank,pos+6,ent\fr[f]\nx[v]*127
			PokeSByte meshbank,pos+7,ent\fr[f]\ny[v]*127
			PokeSByte meshbank,pos+8,ent\fr[f]\nz[v]*127
		Next
	Next
	
	np=BankSize(meshbank):PokeInt meshbank,30,np	;Offset for sequence data
	ResizeBank meshbank,np+8*ent\seqs
	For s=0 To ent\seqs
		PokeShort meshbank,np+8*ent\seqs,ent\seq[s]\start:PokeShort meshbank,np+2+8*ent\seqs,ent\seq[s]\finish
		PokeFloat meshbank,np+4+8*ent\seqs,ent\seq[s]\speed		;Call it one long animseq because I don't know if/how B3D stores those
	Next
	
	fileout=WriteFile(mdxfile)
	WriteBytes(meshbank,fileout,0,BankSize(meshbank))
	CloseFile fileout
	FreeBank meshbank
End Function

Function AnimateMDX(ent.MDX,mode=1,speed#=1,seq=0,tlen=0)	;Animate an MDX
	ent\mode=mode
	ent\speed=speed
	ent\cseq=seq
	ent\dir=Sgn(ent\speed)
	If ent\dir=1
		ent\cframe=ent\seq[seq]\start
	Else
		ent\cframe=ent\seq[seq]\finish
	EndIf
	ent\tlen=tlen
	If tlen
		For v=0 To ent\verts-1
			ent\fr[MAXFRAMES]\vx[v]=VertexX(ent\surf,v):ent\fr[MAXFRAMES]\vy[v]=VertexY(ent\surf,v):ent\fr[MAXFRAMES]\vz[v]=VertexZ(ent\surf,v)
			ent\fr[MAXFRAMES]\nx[v]=VertexNX(ent\surf,v):ent\fr[MAXFRAMES]\ny[v]=VertexNY(ent\surf,v):ent\fr[MAXFRAMES]\nz[v]=VertexNZ(ent\surf,v)
		Next
		ent\tt=0
	EndIf
End Function

Function MDXAnimTime#(ent.MDX)		;Return the current point of the animation as a float, like MD2AnimTime
	If ent\tlen Then Return -1
	Return ent\cframe
End Function

Function MDXAnimLength(ent.MDX,seq=-1)		;Total number of frames held in the MDX, or in the specified sequence
	If seq<0 Or seq>ent\seqs-1
		Return ent\frames
	Else
		Return ent\seq[seq]\finish-ent\seq[seq]\start
	EndIf
End Function

Function MDXAnimating(ent.MDX)		;Return true if MDX is currently animating
	If ent\mode>0 Then Return True:Else Return False
End Function

Function SetMDXFrame(ent.MDX,frame#)	;Manually set animation to a specific point (stops animation)
	ent\mode=0
	ent\cframe=frame
	For v=0 To ent\verts
		VertexCoords ent\surf,v,ent\fr[frame]\vx[v],ent\fr[frame]\vy[v],ent\fr[frame]\vz[v]
		VertexNormal ent\surf,v,ent\fr[frame]\nx[v],ent\fr[frame]\ny[v],ent\fr[frame]\nz[v]
	Next
End Function

Function AddMDXSeq(ent.MDX,fframe,lframe,speed#=1)	;Define an MDX sequence by first and last frames
	ent\seq[ent\seqs]\start=fframe
	ent\seq[ent\seqs]\finish=lframe
	ent\seq[ent\seqs]\speed=speed
	ent\seqs=ent\seqs+1
End Function

Function GetMDXSeq(ent.MDX)		;Return which sequence the MDX is currently playing
	Return ent\cseq
End Function

Function UpdateMDX(updatespeed#=1.0)	;Use this instead of/in addition to UpdateWorld, as that obviously doesn't control MDX animation
	For ent.MDX=Each MDX
		animspeed#=ent\seq[ent\cseq]\speed*ent\speed*updatespeed
		If ent\mode
			If Not ent\tlen
				If ent\mode=2	;Ping-pong
					If animspeed<0 Then animspeed=-animspeed:ent\dir=-ent\dir	;Just reverse the direction, simpler
					ent\cframe=ent\cframe+animspeed*ent\dir
					If ent\cframe>=ent\seq[ent\cseq]\finish-1 Then ent\dir=-1:ent\cframe=ent\seq[ent\cseq]\finish-1
					If ent\cframe<=ent\seq[ent\cseq]\start Then ent\dir=1:ent\cframe=ent\seq[ent\cseq]\start
					
					If ent\dir=1
						frame1=Floor(ent\cframe):frame2=frame1+1:frametween#=ent\cframe-frame1
					Else
						frame1=Ceil(ent\cframe):frame2=frame1-1:frametween#=frame1-ent\cframe
					EndIf
				Else	;Linear
					ent\cframe=ent\cframe+animspeed
					If ent\dir=1	;Going forwards
						If ent\cframe>=ent\seq[ent\cseq]\finish Then ent\cframe=ent\seq[ent\cseq]\start:If ent\mode=3 Then ent\mode=0
						frame1=Floor(ent\cframe)
						If frame1<ent\seq[ent\cseq]\finish-1 Then frame2=frame1+1:Else frame2=ent\seq[ent\cseq]\start
						frametween#=ent\cframe-frame1
					Else	;Going backwards
						If ent\cframe<ent\seq[ent\cseq]\start
							ent\cframe=ent\seq[ent\cseq]\finish-(ent\seq[ent\cseq]\start-ent\cframe)
							If ent\mode=3 Then ent\mode=0
						EndIf
						frame2=Floor(ent\cframe)
						If frame2<ent\seq[ent\cseq]\finish-1 Then frame1=frame2+1:Else frame1=ent\seq[ent\cseq]\start
						frametween#=1-(ent\cframe-frame2)
					EndIf
				EndIf
			Else	;Tween to next sequence
				frame1=MAXFRAMES
				frame2=ent\seq[ent\cseq]\start
				ent\tt=ent\tt+1
				frametween#=Float(ent\tt)/ent\tlen
				If ent\tt>=ent\tlen Then ent\tlen=0:ent\tt=0
			EndIf
			
			f1.TFrame=ent\fr[frame1]:f2.TFrame=ent\fr[frame2]	;When repeatedly accessing type fields, keep the path simple - faster
			For v=0 To ent\verts-1
				vx#=(f1\vx[v]*(1-frametween))+(f2\vx[v]*frametween)
				vy#=(f1\vy[v]*(1-frametween))+(f2\vy[v]*frametween)
				vz#=(f1\vz[v]*(1-frametween))+(f2\vz[v]*frametween)
				nx#=(f1\nx[v]*(1-frametween))+(f2\nx[v]*frametween)
				ny#=(f1\ny[v]*(1-frametween))+(f2\ny[v]*frametween)
				nz#=(f1\nz[v]*(1-frametween))+(f2\nz[v]*frametween)
				VertexCoords ent\surf,v,vx,vy,vz
				VertexNormal ent\surf,v,nx,ny,nz
			Next
		EndIf
	Next
End Function

Function ScaleMDXMesh(ent.MDX,xs#,ys#,zs#)	;ScaleMesh won't work, because the VertexCoords change every frame. Slow!
	For f=0 To ent\frames-1
		For v=0 To ent\verts-1
			ent\fr[f]\vx[v]=ent\fr[f]\vx[v]*xs
			ent\fr[f]\vy[v]=ent\fr[f]\vy[v]*ys
			ent\fr[f]\vz[v]=ent\fr[f]\vz[v]*zs
		Next
	Next
	frame=Floor(ent\cframe)
	For v=0 To ent\verts
		VertexCoords ent\surf,v,ent\fr[frame]\vx[v],ent\fr[frame]\vy[v],ent\fr[frame]\vz[v]
		VertexNormal ent\surf,v,ent\fr[frame]\nx[v],ent\fr[frame]\ny[v],ent\fr[frame]\nz[v]
	Next
End Function

Function CopyMDX.MDX(ent.MDX,newtex=False)
	ent2.MDX=New MDX
	ent2\mesh=CopyMesh(ent\mesh)	;Have to use CopyMesh as CopyEntity would use old mesh data
	ent2\surf=GetSurface(ent2\mesh,1):ent2\verts=CountVertices(ent2\surf)	;Change this if multisurface or multimesh MDX desired
	ent2\texname=ent\texname
	If newtex=True Then ent2\tex=LoadTexture(ent2\texname):Else ent2\tex=ent\tex	;Can share the texture. Remember not to free it in that case!
	EntityTexture ent2\mesh,ent2\tex
	ent2\frames=ent\frames
	For f=0 To ent2\frames-1
		ent2\fr[f]=New TFrame
		For v=0 To ent2\verts-1
			ent2\fr[f]\vx[v]=ent\fr[f]\vx[v]
			ent2\fr[f]\vy[v]=ent\fr[f]\vy[v]
			ent2\fr[f]\vz[v]=ent\fr[f]\vz[v]
		Next
	Next
	ent2\cseq=ent\cseq:ent2\seqs=ent\seqs
	For s=0 To ent2\seqs-1
		ent2\seq[s]=New sequence
		ent2\seq[s]\start=ent\seq[s]\start
		ent2\seq[s]\finish=ent\seq[s]\finish
		ent2\seq[s]\speed=ent\seq[s]\speed
	Next
	ent2\cframe=ent2\seq[ent2\cseq]\start
	Return ent2.MDX
End Function

Function FreeMDX(ent.MDX,freetex=True)
	If freetex And ent\tex<>0 Then FreeTexture ent\tex
	For s=0 To ent\seqs-1
		Delete ent\seq[s]
	Next
	For f=0 To ent\frames-1
		Delete ent\fr[f]
	Next
	FreeEntity ent\mesh
	Delete ent
End Function

Function PeekSShort(bank,offset)	;Return a signed short, range -32767,32767
	s=PeekShort(bank,offset)
	If s>32767 Then Return s-65535:Else Return s
End Function

Function PeekSByte(bank,offset)		;Return a signed byte, range -127,127
	b=PeekByte(bank,offset)
	If b>127 Then Return b-255:Else Return b
End Function

Function PokeSShort(bank,offset,value)	;Store a signed Short
	If value<0 Then value=value+65535
	PokeShort bank,offset,value
End Function

Function PokeSByte(bank,offset,value)	;Store a signed Byte
	If value<0 Then value=value+255
	PokeByte bank,offset,value
End Function

Function MaxRadius#(body)		;Get the largest radius of a mesh (ie. distance of furthest vertex)
	Local r#,cs%,s%,ver%		;Obviously only works on single meshes
	
	For cs=1 To CountSurfaces(body)
		s=GetSurface(body,cs)
		For ver=0 To CountVertices(s)-1
			dx#=VertexX(s,ver)
			dy#=VertexY(s,ver)
			dz#=VertexZ(s,ver)
			dd#=Sqr(dx*dx+dy*dy+dz*dz)
			If r<dd Then r=dd
		Next
	Next
	
	Return r#
End Function

Comments

Yasha2009
Use this program to convert an existing MD2 file to the MDX format used by this system:


And this one converts B3D files to MDX:


I'm well aware that building a whole static mesh anew for each frame is a thoroughly stupid way to get the frame data, but it works, and I don't expect anyone to want to use these in real time!

With a little tweaking you could use a version of this to convert B3D to MD2, as well, which I imagine would be infinitely more useful. Anyway, I hope this is helpful to someone!


Beaker2009
You might gain a little speed by saving the mesh back out as a B3D with one bone per vertex, and animation on each bone (debatable, but worth a try).


Code Archives Forum