Blender Import Guide

From XentaxWiki
Jump to: navigation, search

This document is meant for Blender importer/exporter writers. It explains all you need to know: how to create a mesh, add it to an object, add vertices, faces, vertex colors, UVs, normals, create and assign materials and textures to vertex groups, create bones, assign bones to vertices and add bone weights.

This document will teach you how to import binary 3d files, not text-based. If you want to import text-based files, you'll find the "struct" module we use here completely useless and you might need a text parser like Pyparsing.

Why Blender/Python?

Blender is a powerful 3d modeller. Blender is a great choice if you want to write an importer/exporter as anyone can use your script because the program is completely free, unlike very expensive tools like Max and Maya. In fact users of these expensive tools shouldn't complain, because they can just reexport the data from Blender to a format their own program understands, something which Blender users can't do with Max/Maya scripts without paying. And doing that will require only basic understanding of the Blender GUI.

However Blender scripts are comparably more difficult to write than Max/Maya scripts, especially if you have no experience in programming. The main reason is Blender uses an actual general-purpose programming language called “Python”. What “general-purpose” means is that the language can be used to write any kind of program, from web applications to video games, unlike MaxScript and MEL, which are only used by and inside these 3d modellers. The good news is Python is a very simple language compared to other general-purpose programming languages like C++.

Frankly we don't even need to understand most of Python and most of Blender's Python library to be able to write an importer/exporter.

Firstly, you'll need to learn some Python. Make sure you understand “variables”, “functions”, “lists/tuples”, “dictionaries” and have at least some idea of “classes”. This is a good tutorial: http://www.swaroopch.com/notes/Python

Next all is left to do is to learn the needed functions and classes of Blender's Python library (“library” means collection of modules). A full list is available here:

These are full lists of classes and functions you can use. You don't need to be a programmer to use a computer, the same way you don't need to know more than 5% of these to create an importer/exporter.

As you noticed there are two links, one for Blender 2.4, one for Blender 2.5 and above. The reason is everything was rewritten in Blender 2.5, including the Python library. 2.4 is quickly becoming deprecated, so you should go with Blender 2.5+.

Also note that Blender 2.4 uses Python 2, Blender 2.5 and above use Python 3. These languages are almost the same, but do some things differently. The main differences are:

  • “print” statement doesn't exist in Python 3, only print() function
  • “xrange()” doesn't exist in Python 3, “range()” does the same
  • strings are handled differently

For both you will need to import and use the module “struct”. “struct” is a module for converting data read from binary file to the needed type, like integer, short, long or float.

Basic things every script will need

In this tutorial we will be using helper functions which will call the methods of the "struct" module themselves. This is just to make things cleaner.

# Convenience functions
import struct

# read unsigned byte from file
def read_byte(file_object, endian = '<'):
    data = struct.unpack(endian+'B', file_object.read(1))[0]
    return data

# read unsgned short from file
def read_short(file_object, endian = '<'):
    data = struct.unpack(endian+'H', file_object.read(2))[0]
    return data

# read unsigned integer from file
def read_uint(file_object, endian = '<'):
    data = struct.unpack(endian+'I', file_object.read(4))[0]
    return data

# read signed integer from file
def read_int(file_object, endian = '<'):
    data = struct.unpack(endian+'i', file_object.read(4))[0]
    return data

# read floating point number from file
def read_float(file_object, endian = '<'):
    data = struct.unpack(endian+'f', file_object.read(4))[0]
    return data

So you'll need to pass your file object to the function. Like so:

file = open('filepath/filename.ext', 'rb') # 'rb' - open for reading in binary mode
value = read_uint(file)

The second argument of the functions is used to specify the endianness. Default is little endian ('<').

I'll also like to remind you the useful methods of the "file" class, which you get when you use open().

file.read(size)
file.tell()
file.seek(where, fromwhere)
  • read() is a method for reading bytes from file object and moving the cursor location. "cursor location" is the index of where it will read next time when you'll call read(). When you use read(), you get raw data, Python doesn't know if it's an int, float, ASCII character or whatever, that's why we need the "struct" module. You won't need to mess with read() if you'll use the helper functions above. read() will only be useful when you are reading a string which you are sure is in ASCII, no need to use "struct" for that.
  • tell() is used to tell you the current "cursor location".
  • seek() moves the "cursor location" somewhere else. Useful when you have junk data you need to skip, it's faster to use seek() than read() some data you won't use anyway.
    The first argument of seek() is the index you want to move to, second argument allows you to specify from where to count that index. Default value is 0 which means index relative to beginning of file, specify 1 instead if you want to move from current location.

So let's begin learning what we need from Blender. This tutorial will be for Blender 2.5+. Blender 2.4 sections might be added later.

Creating models in Blender with Python

Creating vertices, edges, faces

The usual way to create vertices, edges and faces is creating lists of vertex positions, list of edge indexes and list of face indexes and passing them as arguments to the function mesh.from_pydata() . Note that you can pass empty lists for edges and faces if you want. As you see, from_pydata() is a method (method - function belonging to class) of the Mesh class. So you'll need to make a Mesh object (object - instance of a class) first. If these terms (“ method”. “class”, “object”, “instance”) confuse you, then you should probably learn some more on classes in Python.

Example:

vertices = [[x, y, z], [x, y, z], [x,y,z], [x,y,z]] # replace “x,y,z” with actual numbers
faces = [[0,1,2], [3,4,5], [6,7,8], [9,10,11]]

mesh = bpy.data.meshes.new('name')
mesh.from_pydata(vertices, [], faces) # [] is in place of edges, is an empty list

The first two lines create Python lists. vertices list contains sublists with 3 numbers, which will be used as x,y,z positions for the vertices. faces list is a collection of indexes, which tells how the vertices in the first list will connect and create triangles. The 3rd line creates a Mesh and third adds vertices and faces to the Mesh from the data we have in our lists.

Like anything else in Blender (like Lights, Curves and Armatures), Meshes need to be assigned to “Objects”. Objects are like nodes. Everything in the 3d scene of Blender are Objects. However the Objects can contain different things, one can contain a Mesh, the other a Light. This allows everything in the 3d scene to the treated the same way, like a Light can be moved around the same way as a Mesh.

So to actually see your Mesh you'll need to create a new Object, assign the Mesh to it and add (“link”) the Object to the current scene:

mesh = bpy.data.meshes.new('name') # again
object = bpy.data.objects.new('name', mesh)
bpy.context.scene.objects.link(object) # link object to scene

And you should be able to see your Mesh, unless you used some odd values for your vertices and faces lists (even then you should at least see a dot which is the pivot of the Object.

Vertex normals

Vertex normals can be added after creating the Mesh.

# assign vertex normals
vindex = 0
for vertex in mesh.vertices:
    vertex.normal = normals[vindex] # where "normals" is a list of normals
    vindex += 1

I'm not sure if there is a point in assigning vertex normals, as I think most 3d modellers, including Blender, will regenerate them anyway.

Vertex colors

To assign vertex colors, you will need to create a “VertexColor layer” for you mesh, then loop through your faces and add the colors to all vertices of each face, like this:

mesh.vertex_colors.new(name = 'VertexColor')
index = 0
for face in mesh.vertex_colors[0].data:
    face.color1 = colors[index]
    face.color2 = colors[index + 1]
    face.color3 = colors[index + 2]
    index += 3

Where colors is a list of sublists containing RGB colors, like so:

colors = [[R,B,G],[R,G,B],[R,G,B]]

Note that colors in Blender are float numbers in the range (0.0, 1.0). if you have an integer in the range (0, 255), just divide your number by 255.

UVs

First you'll need to create an UV map. Then assign the UV values. In this example the UV values are stored in a list named "uvs".

uvtex = mesh.uv_textures.new()
uvtex.name = 'DefaultUV'
for face in range(numoffaces):
    uvtex.data[face].uv1 = uvs[face * 3]
    uvtex.data[face].uv2 = uvs[face * 3 + 1]
    uvtex.data[face].uv3 = uvs[face * 3 + 2]

You will also need to tell that your material/texture uses UV maps, which is only one of the ways to map a texture to a mesh. We will explain that later when we create materials and textures.

Materials

You can create Materials, assign them to Meshes and also specify which faces of the Mesh use which material.

material = bpy.data.materials.new('name')

# lets say you already have lists "ambient", "diffuse", "specular" and "emissive" of RGBA values
# and also variables "shininess" and "texid" (texture id)
# ambient
material.mirror_color = ambient[:-1]
material.ambient = ambient[3]
# diffuse
material.diffuse_color = diffuse[:-1]
material.alpha = diffuse[3]
# specular
material.specular_color = specular[:-1]
# emissive
material.emit = emissive[3]
# shininess
material.specular_hardness = (shininess)

# assign textures to materials
mtex = material.texture_slots.add()
# you'll need to have a list of Texture object already, creating Textures is explained below
mtex.texture = textures[texid]
mtex.texture_coords = 'UV' # set texture mapping mode to UV mapping

# assign material to the Mesh
mesh.materials.append(material)

# to assign this material to specific faces:
for face in mesh.faces[from, to]:
    face.material_index = yourindex

Textures

Textures can be inside the 3d file, or in a separate file. They can be in a format known to Blender, or in a format you'll need to convert manually, maybe with a library like PIL.

If you have texture data inside the 3d file, you can generate a texture in Blender from the pixel data. If the pixel data is a list of RGBA sublists:

image = bpy.data.images.new(name, width, height, alpha = 1)
image.pixels = pixels # "pixels" is a list of RGBA values

# save in blend
image.pack(as_png = True)

# create Texture which will use that image
texture = bpy.data.textures.new(name, type = 'IMAGE')
texture.image = image
texture.use_alpha = True

If you have the textures as separate file in format which Blender understands, you can open them instead:

image = bpy.data.images.load('filename')

Later you can assign the texture to a material like this:

# assign textures to materials
mtex = material.texture_slots.add()
mtex.texture = texture
mtex.texture_coords = 'UV'

Bones, weights

A collection of bones is called a skeleton. Blender uses the term "armature" instead. You'll need to create an Armature, assign it to an Object and add bones to that Armature.

# Create Armature and Object
bpy.ops.object.add(type='ARMATURE', enter_editmode=True)
object = bpy.context.object
object.name = 'name'
armature = object.data
armature.name = 'name'

Next we will add bones to the Armature. Bones are usually stored as matrices, so we will show how to create bones from matrices. Note that you can set the transforms of the bone's base and tip instead, though.

import mathutils

matrix = mathutils.Matrix(data_read_from_file).inverted() # you'll usually need to invert the matrix

bone = armature.edit_bones.new('name')
bone.tail = mathutils.Vector([0,0,1]) # if you won't do it, bone
# will have zero lenght and will be removed immediately by Blender

# assign matrix to bone
bone.transform(matrix)
        
# get out of edit mode when done
bpy.ops.object.mode_set(mode='OBJECT')

Next we will assign weights to the vertices of the Mesh by creating Vertex Group for each bone,

# Vertex group for every bone
for bone in armature.bones:
    vertgroup = object.vertex_groups.new(name=bone.name)

then assign a weight to each vertex when adding it to the particular group:

object.vertex_groups[index].add(vertexindexinmesh, boneweight)

There is no need to specify which bone the vertex is influenced by, just have the bone name and Vertex Group name the same.

Finally, assign your Armature to your Mesh:

TODO

Morps

TODO

Constraints

TODO

Animations

TODO

Adding GUI to your script

The new way of creating GUI in Blender 2.5+ has become pointlessly complicated. Before it was just a function call, now you need to create a class with bunch of variables inside. I don't think you need to learn how these stuff work. Besides, all importers can get by with the same generic file-browser GUI. I'll just tell you what you need to change to use your own name for the importer/exporter in the GUI, don't worry about the rest, just copy-paste it at the end of your script.

Here's a generic importer/exporter GUI code. Your format should appear in "Info window -> File -> Import" or "Info window -> File -> Export" respectively.

from bpy.props import *
from bpy_extras.io_utils import ExportHelper, ImportHelper

class IMPORT_OT_yourformatname(bpy.types.Operator, ImportHelper):
    bl_idname= "import_scene.yourformat"
    bl_description = 'Your description'
    bl_label = "Label for the button in the GUI"
    filename_ext = ".yourextension"
    filter_glob = StringProperty(default="*.defaultextension", options={'HIDDEN'})
    
    filepath= StringProperty(name="File Path", description="Filepath
        used for importing the yourformatname file", maxlen=1024, default="")
    
    def execute(self, context): 
        yourfunctiontorun()
        return {'FINISHED'}
        
    def invoke(self, context, event):
        context.window_manager.fileselect_add(self)
        return {'RUNNING_MODAL'}
        
def menu_func(self, context):
    self.layout.operator(IMPORT_OT_yourformatname.bl_idname, text="your description")

def register():
    bpy.utils.register_module(__name__)
    bpy.types.INFO_MT_file_import.append(menu_func)

def unregister():
    bpy.utils.unregister_module(__name__)
    bpy.types.INFO_MT_file_import.remove(menu_func)

if __name__ == "__main__":
    register()

Just replace "yourformat", "yourformatname", "yourformatextension", ".defaultextension" and "your description" with what you want. "yourfunctiontorun()" is the function which will run when you select your importer from the GUI, so if you haven't already, place the rest of your code in a function.

Transforming your script into a Blender add-on

Before Blender 2.5, scripts like importers/exporters had to be placed in a special folder to be usable by Blender. Now it's still the same, but you don't need to place them manually, you can just "install" them: select them by GUI and click install and Blender will copy the script to the right place automatically. Scripts which can be installed are called "add-ons". You need to add few more things to your script in order for Blender to recognize it as a valid add-on. Add this to the beginning of your script:

bl_info = {
    "name": "Name of the add-on",
    "author": "Name of the author of the add-on, some of these are optional",
    "location": "File > Import > Name of script",
    "description": "Description of the add-on",
    "category": "Import-Export"}

Now you'll need to enable the add-on in Blender like with any other add-on. If you can see your add-on in the add-ons list, but can't enable it, try replacing all tabs in your script with spaces and try again after restarting.

Actual example

TODO