Caio Sabino
16Feb/10Off

Skinned Mesh Exporter for Blender

We all know there are several types of 3D models file formats, and the one I present here is just another one. It turns out that people need to create their own file format or modify existing ones quite often, be it a requirement from their bosses or for educational purposes. I have faced this situation twice, first, when I started studying DirectX 9, I couldn't find a DirectX file format exporter that would work properly with my models on Blender, and that's pretty much how this code was born. The second time, I was part of an IPhone project on the company I work, and our 3d engine was pretty much new and it had no support for skinned meshes so far, so I had to do it myself, and of course an exporter was also necessary. This time the exporter was written for 3DS Max and I simply can't post it here because it is company property now.

I had a hard time finding good examples of the Blender API, and I do think the best way to learn something is by checking the examples and trying them yourself. Omari's DirectXExporter (www.omariben.too.it) helped me a lot in this process, and of course the official Blender documentation site (http://www.blender.org/documentation/249PythonDoc/API_intro-module.html), but still I had to put a lot of effort to create my own file format.

So this post is not about skinned meshes, it is about the exporter. I think Blender is a wonderful tool, I just can't believe how they managed to come up with such a great software and put it out free, but that's another discussion. So I wrote a script to export skinned meshes in a format that my 3D engine would understand. I tried to keep it as simple as possible, and you should be able able to understand most of the code by reading the comments.

Here is a short reference for my file formats:

File Type 1: Mesh Geometry and Bones references:

Description Size in bytes Type
Optmized Flag (irrelevant) 4 Int
Vertex count 4 Int
Index count 4 Int
Index 0 4 Int
... ... ...
Index n 4 Int
Vertex 0 x 4 Float
Vertex 0 y 4 Float
Vertex 0 z 4 Float
Vertex 0 normal x 4 Float
Vertex 0 normal y 4 Float
Vertex 0 normal z 4 Float
Vertex 0 texture u 4 Float
Vertex 0 texture v 4 Float
Vertex 0 bone 0 weight 4 Float
Vertex 0 bone 0 index 4 Int
Vertex 0 bone 1 index 4 Int
... ... ...
Vertex n x 4 Float
Vertex n y 4 Float
Vertex n z 4 Float
Vertex n normal x 4 Float
Vertex n normal y 4 Float
Vertex n normal z 4 Float
Vertex n texture u 4 Float
Vertex n texture v 4 Float
Vertex n bone 0 weight 4 Float
Vertex n bone 0 index 4 Int
Vertex n bone 1 index 4 Int

File Type 2: Bones matrices for animation:

Description Size in bytes Type
Total frames 4 Int
Total bones 4 Int
Bone 0 offset matrix 4*16 16 floats
... ... ...
Bone n offset matrix 4*16 16 floats
Bone 0 head position 4*3 3 floats
Bone 0 tail position 4*3 3 floats
... ... ...
Bone n head position 4*3 3 floats
Bone n tail position 4*3 3 floats
Frame 0, bone 0 pose matrix 4*16 16 floats
... ... ...
Frame 0, bone n pose matrix 4*16 16 floats
... ... ...
Frame n, bone 0 pose matrix 4*16 16 floats
... ... ...
Frame n, bone n pose matrix 4*16 16 floats
Total animations (irrelevant) 4 .Int
Total frames (irrelevant) 4 Int
Bone 0 parent index 4 Int
... ... ...
Bone n parent index 4 Int

As I said, the file format is not the point, and you should easily modify the script to output data in a way that suits you best.

The idea is to have two different files, one that will hold the geometry data and bone references for each vertex, and another that will contain the matrices needed to modify the vertices positions as the animation is played.

Enough with the theory, here is the code for both scripts:

Exporter 1: Geometry and bones influence

# Author: Caio Cintra Sabino (caiocsabino@gmail.com)
# As a reference I used some pieces of code from Omari's DirectXExporter.py version 3.0 (www.omariben.too.it)

import Blender
from Blender import Types, Object, NMesh, Material,Armature,Mesh
from Blender.Mathutils import *
from Blender import Draw, BGL
from Blender.BGL import *
from Blender import Window

import math
import struct, string
from types import *

bone_list =[]
index_list = []
bones_order = []

def event(evt, val):
		if evt == Draw.ESCKEY:
			Draw.Exit()
			return

def button_event(evt):
	if evt == 0:
		t_export = Exporter()
		t_export.start()
	if evt == 1:
		Draw.Exit()

def draw():

		glClearColor(0.55,0.6,0.6,1)
		glClear(BGL.GL_COLOR_BUFFER_BIT)
		#external box
		glColor3f(0.2,0.3,0.3)
		rect(10,402,300,382)

		glColor3f(0.5,0.75,0.65)
		rect(14,398,292,30)
		#--
		glColor3f(0.5,0.75,0.65)
		rect(14,366,292,160)
		#--
		glColor3f(0.5,0.75,0.65)
		rect(14,202,292,60)
		#--
		glColor3f(0.5,0.75,0.65)
		rect(14,138,292,40)
		#--
		glColor3f(0.5,0.75,0.65)
		rect(14,94,292,70)

		glColor3f(0.8,.8,0.6)
		glRasterPos2i(20, 380)
		Draw.Text("C3DE Skinned Mesh Exporter",'large')

		sel_butt = Draw.Button("Start",0,120, 155, 75, 30, "Start")
		exit_butt = Draw.Button("Exit",1,220, 155, 75, 30, "Exit")

def rect(x,y,width,height):
		glBegin(GL_LINE_LOOP)
		glVertex2i(x,y)
		glVertex2i(x+width,y)
		glVertex2i(x+width,y-height)
		glVertex2i(x,y-height)
		glEnd()

def rectFill(x,y,width,height):
		glBegin(GL_POLYGON)
		glVertex2i(x,y)
		glVertex2i(x+width,y)
		glVertex2i(x+width,y-height)
		glVertex2i(x,y-height)
		glEnd()				

Draw.Register(draw, event, button_event)

class Exporter:

	def start(self):
		tex = []

		Window.EditMode(0)

		for obj in Blender.Scene.GetCurrent().objects:
			if obj.type == 'Mesh':
				mesh = obj.data
				self.writeMesh(obj)

			else:
				print("not a mesh %s" % obj.type)
		Draw.Exit()

	def writeMesh(self, obj):
		global index_list,flip_z
		mesh = NMesh.GetRawFromObject(obj.name)
		path = ("..\SkinnedMeshOut0.c3d")
		file = open(path, "wb")						

		file2 = open("..\SkinnedMeshOut.txt", "wb")			

		me = Mesh.New()
		me.getFromObject(obj.name)
		hasTexture = me.faceUV

		objData = obj.data

		vert_uvsU =[]
		vert_uvsV =[]	

		vertex_groups_names = objData.getVertGroupNames()

		#this will hold a list of vertex indices list for every vertex group
		#[ (1,2,3,4), (4,5,6) ... ]
		vertices_groups = []			

		file2.write("Vertex group names: [vertex indices]\n\n")

		#store vertice groups order,
		vertice_group_iterator = 0
		for vertex_group_name in vertex_groups_names:
			bones_order.append(vertex_group_name)
			vertice_group_indices = objData.getVertsFromGroup(vertex_group_name)
			vertices_groups.append(vertice_group_indices)
			file2.write("%s: %s\n" %(vertex_group_name, vertice_group_indices))

		verts = me.verts[:]

		me.verts = verts   

		indices = []
		new_vert_coords = []
		new_vert_normals = []
		new_vert_uvs = []
		indice_it = 0;	

		#list of lists of bones influenced for each vertex
		#[ (1,2), (1,-1), (2,3), (2-1)... ]
		g_bone_indices = []
		g_bone_weights = []			

		#" " "
		for f in me.faces:
			iteration  = range(0,3)		

			for iterator in iteration:
				vertice	= f.v[iterator]
				new_vert_coords.append(vertice.co)
				new_vert_normals.append(vertice.no)
				indices.append(indice_it)
				indice_it += 1

				#will hold a list of bones indices that this vertex is influenced by

				bone_indices = []

				bone_indices.append(-1)
				bone_indices.append(-1)
				bone_indices.append(-1)
				bone_indices.append(-1)
				bone_indices.append(-1)

				bone_weights = []

				bone_weights.append(-1)
				bone_weights.append(-1)
				bone_weights.append(-1)
				bone_weights.append(-1)
				bone_weights.append(-1)																						

				valid_entry_iterator = 0
				bone_indices_iterator = 0
				bone_weight_indices_iterator = 0

				influenceList = objData.getVertexInfluences(vertice.index)

				for vertex_group in vertices_groups:
					for vertex_group_entry in vertex_group:
						if(vertice.index == vertex_group_entry) :
							bone_indices[valid_entry_iterator] = bone_indices_iterator
							bone_weights[valid_entry_iterator] = influenceList[valid_entry_iterator][1]
							valid_entry_iterator += 1
					bone_indices_iterator += 1									

				g_bone_indices.append(bone_indices)
				g_bone_weights.append(bone_weights)

				if hasTexture:
					new_vert_uvs.append(f.uv[iterator])

		#0 means the mesh is not optimized, this has something to do with my own 3d engine
		data = struct.pack("i", (0))
		file.write(data)

		format = "i"
		data = struct.pack(format, indice_it)
		file.write(data)
		file.write(data)

		file2.write("\nnumber of vertices %i\n" % len(new_vert_coords))
		file2.write("\nnumber of indices %i\n" % len(new_vert_coords))

		file2.write("\nVertices; index , x, y, z, nx, ny, nz, u, v, bone 0 weight, bone index 0, bone index 1\n\n")

		vert_iterator = 0;
		for vv in new_vert_coords:
			format = "fffffffffii"                   # one integer
			if hasTexture:
				data = struct.pack(format, vv[0], vv[1], vv[2], new_vert_normals[vert_iterator][0], new_vert_normals[vert_iterator][1], new_vert_normals[vert_iterator][2], new_vert_uvs[vert_iterator][0], (1.0 - new_vert_uvs[vert_iterator][1]), g_bone_weights[vert_iterator][0], g_bone_indices[vert_iterator][0], g_bone_indices[vert_iterator][1]) # pack integer in a binary string
				#file2.write("%f %f %f %f %f %f %f %f %f %i %i\n" % (vv[0], vv[1], vv[2], new_vert_normals[vert_iterator][0], new_vert_normals[vert_iterator][1], new_vert_normals[vert_iterator][2], new_vert_uvs[vert_iterator][0], (1.0 - new_vert_uvs[vert_iterator][1]), g_bone_weights[vert_iterator][0], g_bone_indices[vert_iterator][0], g_bone_indices[vert_iterator][1]))
			else:
				data = struct.pack(format, vv[0], vv[1], vv[2], new_vert_normals[vert_iterator][0], new_vert_normals[vert_iterator][1], new_vert_normals[vert_iterator][2], 0.0, 0.0) # pack integer in a binary string

			file.write(data)
			vert_iterator += 1

Exporter 2: Animation

# Author: Caio Cintra Sabino (caiocsabino@gmail.com)
# As a reference I used some pieces of code from Omari's DirectXExporter.py version 3.0 (www.omariben.too.it)

import Blender
from Blender import Types, Object, NMesh, Material,Armature,Mesh
from Blender.Mathutils import *
from Blender import Draw, BGL
from Blender.BGL import *
from Blender import Window

import math
import struct, string
from types import *

bone_list =[]
index_list = []
mat_dict = {}

bones_order = []
bones_matrix_combinations = []

def event(evt, val):
		if evt == Draw.ESCKEY:
			Draw.Exit()
			return

def button_event(evt): 

	if evt == 0:

		t_export = Exporter()
		t_export.start()
	if evt == 1:
		Draw.Exit()

def draw():
		global animsg,flipmsg,swapmsg,anim_tick
		global flip_z,swap_yz,flip_norm,anim,ticks,speed,recalc_norm,Bl_norm,no_light
		glClearColor(0.55,0.6,0.6,1)
		glClear(BGL.GL_COLOR_BUFFER_BIT)
		#external box
		glColor3f(0.2,0.3,0.3)
		rect(10,402,300,382)
		#--
		#glColor3f(0.3,0.4,0.4)
		#rect(11,399,298,398)
		#--
		glColor3f(0.5,0.75,0.65)
		rect(14,398,292,30)
		#--
		glColor3f(0.5,0.75,0.65)
		rect(14,366,292,160)
		#--
		glColor3f(0.5,0.75,0.65)
		rect(14,202,292,60)
		#--
		glColor3f(0.5,0.75,0.65)
		rect(14,138,292,40)
		#--
		glColor3f(0.5,0.75,0.65)
		rect(14,94,292,70)

		glColor3f(0.8,.8,0.6)
		glRasterPos2i(20, 380)
		Draw.Text("C3DE Skinned Mesh Exporter (2)",'large')

		sel_butt = Draw.Button("Start",0,120, 155, 75, 30, "Start")
		exit_butt = Draw.Button("Exit",1,220, 155, 75, 30, "Exit")

def rect(x,y,width,height):
		glBegin(GL_LINE_LOOP)
		glVertex2i(x,y)
		glVertex2i(x+width,y)
		glVertex2i(x+width,y-height)
		glVertex2i(x,y-height)
		glEnd()

def rectFill(x,y,width,height):
		glBegin(GL_POLYGON)
		glVertex2i(x,y)
		glVertex2i(x+width,y)
		glVertex2i(x+width,y-height)
		glVertex2i(x,y-height)
		glEnd()

Draw.Register(draw, event, button_event)

class Exporter:	

	def start(self):

		tex = []

		Window.EditMode(0)

		iterator = 0		

		for obj in Blender.Scene.GetCurrent().objects:
			print("pass")
			if obj.type == 'Mesh':
				self.writeMeshBonesOrder(obj)
				iterator += 1
			else:
				print("not a mesh %s" % obj.type)

		for obj2 in Blender.Scene.GetCurrent().objects:
			if obj2.type == 'Armature':
				self.writeBones(obj2)
		print "...finished"
		Draw.Exit()

	def writeMeshBonesOrder(self, obj):		

		mesh = NMesh.GetRawFromObject(obj.name)								

		me = Mesh.New()              # Create a new mesh

		me.getFromObject(obj.name)    # Get the object's mesh data

		hasTexture = me.faceUV							

		vertex_groups_names = obj.data.getVertGroupNames()												

		for vertex_group_name in vertex_groups_names:
			bones_order.append(vertex_group_name)	

	def writeBones(self, obj):				

		file = open("..\SkinnedMeshOutBones.c3d", "wb")
		file2 = open("..\SkinnedMeshOutBones.txt", "wb")		

		armature_obj = obj.getData()				

		bones = armature_obj.bones.values()			

		file2.write("bones original position\n\n")
		for bone in bones:
			print("bones %s  %s\n\n\n" % (bone.name, bone.matrix))
			file2.write("%s %s\n\n" % (bone.name,	bone.matrix))

		totalBones = len(bones_order)		

		#define here the animation frames
		frameStart = 1
		frameEnd = 60

		totalFrames = frameEnd - frameStart

		format = "ii"
		data = struct.pack(format, totalFrames, totalBones) # pack integer in a binary string
		file.write(data)		

		bones = armature_obj.bones.values()			

		#offset matrix for each bone

		for bone_name in bones_order:
			for bone in bones:
				if(bone.name == bone_name) :
					t_matrix = bone.matrix["ARMATURESPACE"]
					t_matrix2 = bone.matrix["BONESPACE"]

					#if(bone.name == "Pelvis" or bone.name == "Back") :
					print("\n\n-----BONE %s in armature space %s \n" % (bone.name, t_matrix))
					print("\n\n-----BONE %s in local space %s \n" % (bone.name, t_matrix2))

					it = range(0,16)
					for count in it:
						data = struct.pack("f", t_matrix[count/4][count%4])
						file.write(data)

		#bones heads and tails

		for bone_name in bones_order:
			t_poseObject = obj.getPose()									

			#HERE
			t_poseBones = t_poseObject.bones
			bone = t_poseBones[bone_name]
			print("bone %s head %f %f %f" % (bone_name, bone.head[0], bone.head[1], bone.head[2]))
			print("bone %s tail %f %f %f" % (bone_name, bone.tail[0], bone.tail[1], bone.tail[2]))
			data = struct.pack("ffffff", bone.head[0], bone.head[1], bone.head[2],bone.tail[0], bone.tail[1], bone.tail[2]) # pack integer in a binary string
			file.write(data)	

		iterator = 0												

		framesIteration = range(frameStart,frameEnd)

		Blender.Set('curframe', frameStart)

		file2.write("\n\nbones new positions\n\n")
		for count in framesIteration:

			armature_obj = obj.getData()
			file2.write("\n\nframe %i \n\n" % (count))

			Blender.Set('curframe', count)
			poseObject = obj.getPose()									

			#HERE
			poseBones = poseObject.bones									

			iterator = 0

			for bone_name in bones_order:				

				#localPose = poseBones[bone_name]

				#Blender api provides this local matrix from the PoseBone object, it represents the final transformation
				#the vertex will need to use in order to modify it's position relative to the bone position. Depending
				#on the bone's weight of the vertex, you just need to balance the matrices according to their weights.
				#Example using a mesh influenced by 2 bones at maximum:

				#finalVertexPosition = (weight0 * (originalPosition * poseMatrixForBone0)) + (weight1 * (originalPosition * poseMatrixForBone1))

				finalMatrix = poseBones[bone_name].localMatrix	

				file2.write("pose %s %s\n\n" % (bone_name,	poseBones[bone_name].localMatrix))																														

				format = "ffffffffffffffff"

				data = struct.pack(format, finalMatrix[0][0], finalMatrix[0][1], finalMatrix[0][2], finalMatrix[0][3], finalMatrix[1][0], finalMatrix[1][1], finalMatrix[1][2], finalMatrix[1][3], finalMatrix[2][0], finalMatrix[2][1], finalMatrix[2][2], finalMatrix[2][3], finalMatrix[3][0], finalMatrix[3][1], finalMatrix[3][2], finalMatrix[3][3]) # pack integer in a binary string
				file.write(data)								

				iterator += 1											

		format = "ii"
		data = struct.pack(format, 1, totalFrames) # pack integer in a binary string
		file.write(data)

		#writes the parent bone index for every bone. -1 for orphan bones
		for bone in bones:

			parent = bone.parent
			iterator = 0
			for bone2 in bones:
				if(parent == bone2):
					print("bones parent %s %s %i \n\n\n" % (bone.name, bone2.name, iterator))
					data = struct.pack("i", iterator)
					file.write(data)
				iterator += 1

			if(str(parent) == "None"):
				print("orphan bones  %s \n\n\n" % (bone.name))
				data = struct.pack("i", -1)
				file.write(data)

			#print("bones parent %s %s %s\n\n\n" % (bone.name,	parent, bone.matrix))

		Draw.Exit()

Before running the script:

  • Make sure your mesh has all faces converted to triangles.
  • Make sure the mesh vertex groups have the same name as the bones in the armature.
  • Make sure that every vertex group in the mesh has a correspondent bone in the armature.
  • Make sure the model is in the "rest" position when you use the first script.
  • Make sure the model is NOT in the "rest" position when you use the second script.
  • Keep your file simple. The exporter should work with high poly count meshes, but I am not a blender expert, so if you use some advanced features it might break something. The script supports just pure geometry, uv texturing and bones.

How to run the script:

  1. In Blender, open a new Text Editor view.
  2. Open the first script in this view.
  3. Put the Model in the "rest" position
  4. Press Alt + p.
  5. Press Start.
  6. Disable the "rest" position.
  7. Open the second script in the Text Editor.
  8. Press Alt + p.
  9. Press Start.

You can test the scripts on the file below. I got it probably from some Blender model repository  long time ago, but I simply lost the site of the author after I have modified it so much. If you know the link to where people can download the original file, please post a comment.

characterMotion.blend

16Feb/10Off

The Start

Hello,

I am not much of a blog guy, but I decided to start this one to post some of the stuff I create on my free time. Most of it will be game programming code excerpts and ideas that I may think somebody will find useful. You can use anything you find here, just provide the link to this site. I may also use some code from other people that I think somebody may benefit from.

Cheers,

Filed under: Uncategorized 1 Comment