Blender exporter
Jun
20
2013

My first 3D game had a little space ship in it. I modeled this in blender, and then started looking for a way to get it into my game, and it felt natural to simply have a look in the list of exporters if there was anything there I could parse while simultaneously containing what I needed, such as vertex normals and texture coordinates.

I found one, a plain text format called Ac3d and built a parser for it. It was sufficient for my needs and I that's what I went with for my first project. And a couple of other projects after that. A while ago though, I spotted a forum post asking the same question, what file format is a good format for games? There were a bunch of suggestions, but one answer really stood out and got me thinking: Write an exporter for blender, it's got a decent python API and you can get exactly what you need. I figured I'd research it a bit. I wasn't disappointed, and this is what I'll use for Choco.

Blender Python API

The blender Python API is fairly well documented on the Blender site (link is to version 2.63, make sure you have the documentation matching your version). Along with your blender installation you'll also find a bunch of export scripts to get inspiration from.

The entry point for accessing objects is 'bpy.data.objects', which conveniently allows you to create a mesh object from any object using the 'to_mesh' function, which allows you apply modifiers and select whether you want to 'preview' version or the 'render' version (which is also why you should use this method even if it already is a mesh object). I use the 'preview' version, as I dont have 'render' versions, and I want to see the model I will actually export. You can also use the same model with different settings for rendering (such as subdivision resolution) to create nice graphics for your website. You might want to remove the mesh objects afterwards to not keep the copies in memory. Incidentally, if you save the file, the objects you created will be saved along with it if you don't delete them. Keep in mind that you cannot convert cameras and lamps to meshes, so check the type first to make sure it's something you want to export. You can of course elect to simply export a single object, perhaps specified by name on the command line (see below), or even create a separate file for each object directly.

for o in bpy.data.objects:
  if o.layers[0] and o.type in ['MESH', 'CURVE', 'SURFACE']:
    obs.append(o)

In the created mesh object you'll find 'vertices' and 'tessfaces' to start with, the vertices probably speak for themselves, containing coordinates and normals. Tessfaces ("tesselated faces") contain each face, which may be a quad or triangle, with indexes to the vertices used, face normal etc. This should be enough to get you started exporting your models.

for o in obs:
  m = o.to_mesh(context.scene, True, 'PREVIEW')

  for v in m.vertices:
    ...

  for f in m.tessfaces:
    ...

  bpy.data.meshes.remove(m)

Writing data

Python has a nifty little API for building binary data, called 'struct'. I assume it was originally intended to pack data into aligned structures for immediate use in C. However, it allows you to specify byte ordering (and if you choose to, it'll disable alignment) which would make it independent of the build platform. It supports a variety of types, including floats, doubles, and various integer formats. In Choco, I use little-endian byte ordering (explicitly byte swapped to the current platform, I don't assume I'm on a little-endian system, you shouldn't either). Remember that byte ordering matters for floats as well.

from struct import pack

for v in m.vertices:
  file.write(pack('<3f', v.co[0], v.co[1], v.co[2]))
  file.write(pack('<3f', v.normal[0], v.normal[1], v.normal[2]))

for f in m.tessfaces:
  file.write(pack('<3I',f.vertices[0],f.vertices[1],f.vertices[2]))
  if len(f.vertices) > 3: # Convert quads to triangles
    file.write(pack('<3I',f.vertices[2],f.vertices[3],f.vertices[0]))

A plain text format might in some cases be preferable, as you can read it and try to figure out what went wrong. But, at the same time, a binary format doesn't have any rounding errors due to the plain text representation (not that floats don't have problems in general, but this one in particular we can avoid). If you think about your file layout a bit, make it align nicely with a multiples of say 8 or 16 bytes or whatever your favorite hex-editor works with, binary files can be quite readable. A binary representation is also bound to be more compact, although a plain text file can of course be compressed with ease.

Of course, right about now, you'll realize that little subdivision modifier and your extrusion path resolution caused a vertex and face count explosion, pushing your little tiny object into the megabyte range, and you start pondering optimizations. Sure, some easy optimizations might be worth it, like storing vertex indecies using 16 or even 8 bits if possible. Other optimizations are of course possible, like finding an optimal set of triangle strips or fans rather than individual triangles, but that's a whole different post. Have another look at your models though, do you really need all that resolution?

The boiler plate

Naturally, as with pretty much all plug-ins everywhere, you'll require some boilerplate to make your exporter appear in blender (in menus, or, for head-less operation, in the Python API). Fortunately, it's not a whole lot, and the easiest way is to just have a look at existing exporters. This is what I have in mine, heavily influenced by the DirectX exporter

from bpy.props import StringProperty, BoolProperty
class ChocoExporter(bpy.types.Operator):
  """Export using Choco model format (.cmo)"""

  bl_idname = 'export.choco'
  bl_label = 'Export Choco'

  path = StringProperty(subtype='FILE_PATH')
  verbose = BoolProperty (name="Verbose",
    description="Log verbose info to console",
    default=False)

  def execute(self, context):
    path = bpy.path.ensure_ext (self.path, ".cmo")

    config = ChocoExportConfiguration(path = path,
      verbose = self.verbose)

    export_choco(context, config)
    return {"FINISHED"}

  def invoke(self, context, event):
    if not self.path:
      self.path = bpy.path.ensure_ext(bpy.data.filepath,
                                      ".cmo")
    wm = context.window_manager
    wm.fileselect_add(self)
    return {"RUNNING_MODAL"}

def menu_func(self, context):
  self.layout.operator(ChocoExporter.bl_idname,
                       text="Choco (.cmo)")

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

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

if __name__ == "__main__":
  register()

Note that the 'Property' fields are evaluated by Blender, and will appear as options on the export screen, as well as in the Python API as named parameters to the export function, which is neat.

A headless blender for your makefile needs

Blender works very well without a GUI. It simply loads a blend-file and executes a python script, which pretty much covers anything you can do in the UI as well. The basic command line looks like this (check out 'blender --help' to figure out what else might be of interest).

blender -b <file.blend> -P <script.py>

In order to pass parameters to your script append '--' to stop Blenders argument parsing and the rest can be accessed through 'sys.argv' as normal in Python.

import bpy
import sys

try:
  args = sys.argv[sys.argv.index('--')+1:]
  bpy.ops.export.choco(path=args[0], verbose=0)
except ValueError as e:
  print("Unable to export: {}".format(e))
  sys.exit(1)

So to wrap it up, if it wasn't clear already, here's how I call it on the command line

  blender -b <file.blend> -P <script.py> -- <target.cmo>

That said, you may of course use the GUI too, and simply select export from the file menu. Remember though, you need to activate your exporter from the addons page on the user preference screen before you can use it. Note that you need to save your preferences, or your exporter will not be enabled next time, or when you run your headless build scripts.

As mentioned, Blender allows pretty much anything to be done headless using a Python script, so as a final note, if you ever find yourself rendering scenes to be used as loading screens, baking textures, or whatever you're doing, it might be a good idea to have another look at that Python API.

comments powered by Disqus

Categories

Archives