Blender 3D Scene Exporter

6837d514b487de395be51432d9cdd078
0
TheNut 179 Aug 20, 2011 at 09:00

This is not a fancy new algorithm or super fast technique, but rather a simple exporter for the Blender 3D modelling software. In my quest to find the perfect file format, I have written and supported importers and exporters for pretty much any format you can think of. Yes, I even wrote my own TrueType importer :) At first I was going to settle with Blender’s Collada exporter, but after all the hard work I went through I realized Blender (as of version 2.58) doesn’t fully support Collada yet. The kind of information I needed just wasn’t there, specifically animations and texture transformations.

Rather than sit around waiting for the kind of support I need, I decided to learn Python and write my own exporter for Blender. The python code and a sample output file are posted below. It sort of follows the same organizational structure as Collada, but it’s much more straightforward and less bloat.

The point I want to make here is that Blender is an awesome modelling and editing package. I’ve recently been using it as my editor of choice for setting up my game scenes and environments. Collada is about the closest anyone can get to an off-the-shelf scene file format without inventing your own, but it’s a massive format and as I said, not fully supported in Blender yet. I wanted to share my results with others who may either benefit from the format itself or simply learn from my python code how to write your own exporter for Blender. Blender’s Python API documentation isn’t bad, but there are some holes in it, especially when it comes to matching armatures, objects, and animations. Looking at my code should cut down your search significantly.

The Python code is posted below. You can download the code here or copy/paste the code below.

# NUT Blender Export Script
#
# Author:       Nathaniel Meyer
# Website:      http://www.nutty.ca
#
# Notes:
# 1. Built with Blender version 2.58, but works in 2.59 as well.
#
# 2. To install this script, place it in your <BlenderHome>/<BlenderVersion>/scripts/addons/ folder.
#   Open the "User Preferences" window, select Add-Ons, and search for the script
#   "Import-Export: NUT Scene Format(.nut)".
#
# 3. This script is provided as-is. Do whatever you want with it. Use it, modify it, call it your own,
#   sell it, gift wrap it for your mother, whatever. It's 100.0% free, with no added GNU.


bl_info = {
    "name": "NUT Scene Format (.nut)",
    "author": "Nathaniel Meyer",
    "version": (1, 0),
    "blender": (2, 5, 8),
    "api": 38019,
    "location": "File > Export > Nut (.nut)",
    "description": "Export NUT Scene Format (.nut)",
    "warning": "",
    "wiki_url": "http://www.nutty.ca",
    "tracker_url": "http://www.nutty.ca",
    "category": "Import-Export"}


# Blender and Python Imports
import bpy
from bpy.props import StringProperty, BoolProperty
from bpy_extras.io_utils import ExportHelper
import math

# Convert RAD to degrees
RAD = 180.0 / math.pi


# Summary:
# Export cameras.
#
# @param file
#   File stream to output.
def ExportCameras(file):
    if bpy.data.cameras and (len(bpy.data.cameras) > 0):
        file.write("\t<cameras>\n")
        for camera in bpy.data.cameras:
            file.write("\t\t<camera name=\"%s\" type=\"%s\">\n" % (camera.name, "perspective" if camera.type == "PERSP" else "orthographic"))
            file.write("\t\t\t<resolution x=\"%d\" y=\"%d\" />\n" % (bpy.data.scenes[0].render.resolution_x, bpy.data.scenes[0].render.resolution_y))
            file.write("\t\t\t<angle>%s</angle>\n" % (camera.angle * RAD))
            file.write("\t\t\t<clip near=\"%f\" far=\"%f\" />\n" % (camera.clip_start, camera.clip_end))
            file.write("\t\t\t<lens>%s</lens>\n" % camera.lens)
            file.write("\t\t</camera>\n")
        file.write("\t</cameras>\n")


# Summary:
# Export lights. Blender calls them "lamps".
#
# @param file
#   File stream to output.
def ExportLights(file):
    if bpy.data.lamps and (len(bpy.data.lamps) > 0):
        file.write("\t<lights>\n")
        for lamp in bpy.data.lamps:
            file.write("\t\t<light name=\"%s\" type=\"%s\">\n" % (lamp.name, "point" if lamp.type == "POINT" else "ambient" if lamp.type == "SUN" else "directional" if lamp.type == "SPOT" else "area" if lamp.type == "AREA" else ""))
            file.write("\t\t\t<colour r=\"%f\" g=\"%f\" b=\"%f\" />\n" % (lamp.color[0], lamp.color[1], lamp.color[2]))
            if lamp.falloff_type == "INVERSE_LINEAR":
                file.write("\t\t\t<attenuation l=\"%f\" q=\"%f\" />\n" % (1.0 / lamp.distance, 0.0))
            elif lamp.falloff_type == "INVERSE_SQUARE":
                file.write("\t\t\t<attenuation l=\"%f\" q=\"%f\" />\n" % (0.0, 1.0 / (lamp.distance * lamp.distance)))
            elif lamp.falloff_type == "LINEAR_QUADRATIC_WEIGHTE":
                file.write("\t\t\t<attenuation l=\"%f\" q=\"%f\" />\n" % (lamp.linear_attenuation, lamp.quadratic_attenuation))
            file.write("\t\t</light>\n")
        file.write("\t</lights>\n")


# Summary:
# Export images. Images are used by textures.
#
# @param file
#   File stream to output.
def ExportImages(file):
    if bpy.data.images and (len(bpy.data.images) > 0):
        file.write("\t<images>\n")
        for image in bpy.data.images:
            file.write("\t\t<image name=\"%s\" url=\"%s\" />\n" % (image.name, image.filepath.replace("\\", "/")))
        file.write("\t</images>\n")


# Summary:
# Export textures. Only image textures are supported.
#
# @param file
#   File stream to output.
def ExportTextures(file):
    if bpy.data.textures and (len(bpy.data.textures) > 0):
        file.write("\t<textures>\n")
        for texture in bpy.data.textures:
            file.write("\t\t<texture name=\"%s\" type=\"%s\">\n" % (texture.name, texture.type.lower()))
            if (texture.type == "IMAGE") and texture.image:
                file.write("\t\t\t<image>%s</image>\n" % texture.image.name)
            file.write("\t\t</texture>\n")
        file.write("\t</textures>\n")


# Summary:
# Export materials.
#
# @param file
#   File stream to output.
def ExportMaterials(file):
    if bpy.data.materials and (len(bpy.data.materials) > 0):
        file.write("\t<materials>\n")
        for material in bpy.data.materials:
            file.write("\t\t<material name=\"%s\">\n" % material.name)
            file.write("\t\t\t<diffuse r=\"%f\" g=\"%f\" b=\"%f\" />\n" % (material.diffuse_color[0], material.diffuse_color[1], material.diffuse_color[2]))
            file.write("\t\t\t<specular r=\"%f\" g=\"%f\" b=\"%f\" />\n" % (material.specular_color[0], material.specular_color[1], material.specular_color[2]))
            file.write("\t\t\t<shininess>%f</shininess>\n" % material.specular_hardness)
            file.write("\t\t\t<alpha>%f</alpha>\n" % material.alpha)
            for texSlot in material.texture_slots:
                if texSlot:
                    file.write("\t\t\t<texture name=\"%s\">\n" % texSlot.texture.name)
                    file.write("\t\t\t\t<offset x=\"%f\" y=\"%f\" z=\"%f\" />\n" % (texSlot.offset[0], texSlot.offset[1], texSlot.offset[2]))
                    file.write("\t\t\t\t<scale x=\"%f\" y=\"%f\" z=\"%f\" />\n" % (texSlot.scale[0], texSlot.scale[1], texSlot.scale[2]))
                    file.write("\t\t\t</texture>\n")
            file.write("\t\t</material>\n")
        file.write("\t</materials>\n")


# Summary:
# Export geometries. Blender stores UV coordinates per-index, not
# per-vertex. If you have 4 vertices and 6 face indices, there are 6 UV
# coordinates. If you wish to convert from per-index to per-vertex,
# you must reconstruct the index list with unshared vertices. This
# takes away render efficiency.
#
# @param file
#   File stream to output.
def ExportMeshes(file):
    if bpy.data.meshes and (len(bpy.data.meshes) > 0):
        file.write("\t<geometries>\n")
        for mesh in bpy.data.meshes:
            file.write("\t\t<geometry name=\"%s\">\n" % mesh.name)
            
            # Vertices
            maxBonePerVertex = 0
            vertCount = len(mesh.vertices)
            file.write("\t\t\t<vertices count=\"%d\">" % vertCount)
            for i in range(vertCount):
                if i > 0:
                    file.write(" ")
                file.write("%f %f %f" % (mesh.vertices[i].co[0], mesh.vertices[i].co[1], mesh.vertices[i].co[2]))
                if mesh.vertices[i].groups and (len(mesh.vertices[i].groups) > 0):
                    groupCount = len(mesh.vertices[i].groups);
                    if groupCount > maxBonePerVertex:
                        maxBonePerVertex = groupCount
            file.write("</vertices>\n")
            
            # UV Coordinates
            for uvTexture in mesh.uv_textures:
                file.write("\t\t\t<uv count=\"%d\">" % len(uvTexture.data))
                uvDataCount = len(uvTexture.data)
                for i in range(uvDataCount):
                    # UV per face index, not per vertex
                    if i > 0:
                        file.write(" ")
                    uvCount = len(uvTexture.data[i].uv)
                    for j in range(uvCount):
                        if j > 0:
                            file.write(" ")
                        file.write("%f %f" % (uvTexture.data[i].uv[j][0], uvTexture.data[i].uv[j][1]))
                file.write("</uv>\n")
            
            # Normals - Save space and calculate them later
            #file.write("\t\t\t<normals count=\"%d\">" % vertCount);
            #for i in range(vertCount):
            #   if i > 0:
            #       file.write(" ")
            #   file.write("%f %f %f" % (mesh.vertices[i].normal[0], mesh.vertices[i].normal[1], mesh.vertices[i].normal[2]))
            #file.write("</normals>\n")
            
            # Indices. Polygons are separated by commas. It is intuitive to know when you're
            # dealing with a triangle or a quad.
            faceCount = len(mesh.faces);
            file.write("\t\t\t<indices count=\"%d\">" % faceCount);
            for face in range(faceCount):
                if face > 0:
                    file.write(",")
                count = len(mesh.faces[face].vertices)
                for i in range(count):
                    file.write("%d" % mesh.faces[face].vertices[i])
                    if i != (count - 1):
                        file.write(" ")
            file.write("</indices>\n")
            
            # Bone groups and weights. Bone groups and weights are separated by commas and represent
            # the bone groups and weights for a particular vertex.
            # Ex: 0 0.5 3 0.4 2 0.1, ...  <- This means the first vertex has 3 bones that effect it.
            # They are groups 0, 3, and 2. The weights for each of these bones are listed beside them,
            # 0.5, 0.4, and 0.1 respectively. Some vertices may have more or fewer bones than others.
            # maxBonePerVertex attribute tells you what the maximum number of bones are for any vertex.
            if maxBonePerVertex > 0:
                boneOutput = False
                file.write("\t\t\t<bones count=\"%d\" maxBonePerVertex=\"%d\">" % (vertCount, maxBonePerVertex))
                for i in range(vertCount):
                    if i > 0:
                        file.write(",")
                    if mesh.vertices[i].groups and (len(mesh.vertices[i].groups) > 0):
                        groupCount = len(mesh.vertices[i].groups)
                        for j in range(groupCount):
                            file.write("%d %f" % (mesh.vertices[i].groups[j].group, mesh.vertices[i].groups[j].weight))
                            if (j + 1) < groupCount:
                                file.write(" ")
                file.write("</bones>\n")
                
                # Find the object with this mesh and extract the vertex groups from it
                for object in bpy.data.objects:
                    if object.data == mesh:
                        # Vertex Groups
                        if object.vertex_groups and (len(object.vertex_groups) > 0):
                            file.write("\t\t\t<groups>\n")
                            for vertexGroup in object.vertex_groups:
                                file.write("\t\t\t\t<group name=\"%s\" index=\"%d\" />\n" % (vertexGroup.name, vertexGroup.index))
                            file.write("\t\t\t</groups>\n")
                            
                            break;
            
            file.write("\t\t</geometry>\n")
        file.write("\t</geometries>\n")


# Summary:
# This method will traverse up the bone's parent until the root bone is found.
#
# @param bone
#   Bone to traverse up until the root bone is found.
# @return
#   Reference to the root bone.
def GetRootBone(bone):
    while bone:
        if bone.parent:
            bone = bone.parent;
        else:
            break;
    return bone


# Summary:
# Export bones. Called directly by ExportSkeletons.
#
# @param file
#   File stream to output.
# @param bone
#   Current bone to output.
# @param relative
#   Set true to output relative coordiantes, otherwise false for absolute coordinates.
# @param tab
#   String to store XML tabbing to keep things ordered.
def ExportBones(file, bone, relative, tab):
    if relative and bone.parent:
        # Calculate relative position
        rx = (bone.head_local[0] - bone.parent.tail_local[0]) + (bone.parent.tail_local[0] - bone.parent.head_local[0]);
        ry = (bone.head_local[1] - bone.parent.tail_local[1]) + (bone.parent.tail_local[1] - bone.parent.head_local[1]);
        rz = (bone.head_local[2] - bone.parent.tail_local[2]) + (bone.parent.tail_local[2] - bone.parent.head_local[2]);

        file.write("%s<bone name=\"%s\" x=\"%f\" y=\"%f\" z=\"%f\"" % (tab, bone.name, rx, ry, rz))
    else:
        # Write out absolute position (relative to armature)
        file.write("%s<bone name=\"%s\" x=\"%f\" y=\"%f\" z=\"%f\"" % (tab, bone.name, bone.head_local[0], bone.head_local[1], bone.head_local[2]))

    # Process bone children
    if bone.children and (len(bone.children) > 0):
        file.write(">\n")
        for childBone in bone.children:
            ExportBones(file, childBone, relative, tab + "\t");
        file.write("%s</bone>\n" % tab)
    else:
        file.write(" />\n")


# Summary:
# Export skeletons. Blender calls them armatures.
# 1. Blender armatures can have multiple root bones (need to find them).
# 2. bone.head_local is relative to armature.
# 3. bone.head is relative to parent, but doesn't appear correct.
#
# @param file
#   File stream to output.
def ExportSkeletons(file):
    if bpy.data.armatures and (len(bpy.data.armatures) > 0):
        file.write("\t<skeletons>\n")
        for armature in bpy.data.armatures:
            file.write("\t\t<skeleton name=\"%s\">\n" % armature.name)
            
            # Store a list of processed root bones
            rootBoneList = list()
            
            # Find root bones
            # Blender's bone list is a flat array, so we need to reconstruct the hierarchy.
            if armature.bones and (len(armature.bones) > 0):
                for bone in armature.bones:
                    # Get the root bone using recursion
                    rootBone = GetRootBone(bone);
                    
                    # Process only unprocessed root bones, avoid duplicates.
                    if rootBone not in rootBoneList:
                        rootBoneList.append(rootBone);
                        ExportBones(file, rootBone, True, "\t\t\t");
            file.write("\t\t</skeleton>\n")
        file.write("\t</skeletons>\n")


# Summary:
# Export animations. Blender calls them actions.

Attached is a sample output containing a fully rigged skeleton with a basic walk animation. It’s XML based, easy to load, and contains most of what you need to setup a game scene or several scenes.

8 Replies

Please log in or register to post a reply.

A638aa42130293f319eda7fa4ba121f4
0
fireside 141 Aug 20, 2011 at 21:49

Nice job. I thought the Collada exporter had animations but maybe not. The FBX exporter works well with Unity aside from a few minor glitchy things when updating. It actually reads a blend file, but it’s using the FBX exporter. I haven’t used 2.58 yet, still using the older version. Everything is so different. I can’t find anything. It’s almost like starting over.

6837d514b487de395be51432d9cdd078
0
TheNut 179 Aug 21, 2011 at 16:43

I originally made that assumption too. Only wish I investigated it before I jumped in. The lack of animations for me wasn’t so bad though. I’ve been using BVH for quite some time and the format works well, albeit file sizes can get quite large. The lack of texture transformations was the main reason I had to dump it. I rely heavily on detailed texturing and I can’t stand having to modify the UV coordinates to simulate the same effect.

Are you still using Blender 2.4X? If you are, you are missing out on some major new stuff in 2.5 that totally puts the older version to shame. The new UI isn’t that hard to pick up. If you watch a couple tutorials from Andrew Price, you’ll pick it up rather quickly. The new AO bake operation and new UV unwrapping tools are amazing. The smoke effects are amazing too. Spent 5 hours rendering an explosion effect and the results were fantastic.

A638aa42130293f319eda7fa4ba121f4
0
fireside 141 Aug 21, 2011 at 18:57

Yes, I’m using 2.49. Thanks for the link. I need to get going on some tutorials and get it over with, I know. I’m sure it won’t be that bad once I get on with it. Have you checked out MakeHuman, lately? It exports rigged models to Blender now. Nice for realistic models which are a pain to do manually. I haven’t actually tried it in Blender, just looked at it in Makehuman.

3c5be51fdeec526e1f232d6b68cc0954
0
Sol_HSA 119 Aug 22, 2011 at 07:13

Back when I looked at blender to use for demo purposes I also went through every single format it exports, and found that none of the exported formats actually works. It’s as if whoever wrote the exporter for any specific format had some feature set they themselves needed, wrote support for that and then stopped.

So, I wrote my own exporter too. For a custom format. That only has the features I needed. =)

It was in a way a fun experience, to (re-)learn python, figure out how to turn whatever blender has to triangles in a sane way, and so on and so forth.. in the end I didn’t end up using it for anything though.

A638aa42130293f319eda7fa4ba121f4
0
fireside 141 Aug 22, 2011 at 10:25

Back when I looked at blender to use for demo purposes I also went through every single format it exports, and found that none of the exported formats actually works.

I think a lot of it is because who-ever wrote the exporter doesn’t bother to keep it updated with new versions, also. That’s one of the problems of open source software, it’s only updated if someone feels like it or not. It generally works good for the main software, but the kind of branch stuff like exporters doesn’t work nearly as well. The md2 format hasn’t worked in ages. It still comes in handy once in a while, but I can see why no one bothers to update. I guess the nice thing is, it’s open source and you can get it there and make it work for your purposes. Although, most modelers have export kits. Blender is big enough now that a lot of engines make sure there is something that works, which is better than the older days. I remember giving up on the 3d engine because it was so buggy, but they look like they’ve really come a long way with it. No one was working on the engine at all for quite a few years except the physics engine, which was updated constantly. It looks quite usable now. Unfortunately, I got used to extending classes and it’s more component based like Unity. I guess right now, I’m looking more for a portal to drop a small game off than anything when it comes to an engine. Kongregate seems the best for that so far.

6837d514b487de395be51432d9cdd078
0
TheNut 179 Aug 22, 2011 at 10:54

I haven’t used MakeHuman. I do have free rigged high poly humans if I ever want to use them (from the Open Human project). Normally I don’t have a problem rigging and animating with Blender though. My characters are robotic (mechs, cyborgs), so I don’t have to dabble with lip syncing or muscular movements. Makes life a bit easier. Plus if I ever need high quality animations, I just turn to the Carnegie Mellon motion capture database. 5.5 GB of free animations in BVH format.

A638aa42130293f319eda7fa4ba121f4
0
fireside 141 Aug 22, 2011 at 18:31

I didn’t know about the motion capture database, thanks for the link. I’ve never messed around with the bvh format. I don’t normally do realistic games, but I’m considering doing an adventure in Flash that might use some more realistic sprites to improve story. I’ve kind of relegated myself to puzzle games, but I may do a puzzle/adventure type thing something like tomb raider, only a lot simpler and with more re-usable background elements, not sure. The puzzles for those always end up being like Sokoban. I may try a realistic adventure in Unity, also. You should just take a look at MakeHuman sometime. It’s really impressive for changing facial features, age, sex, all kinds of things and spitting out a model. Really cool, but I haven’t actually used it in a game.

653b661569369adf8f954a05aeebe633
0
DevPlayer 101 Nov 25, 2011 at 05:06

Not intending to be too nit picky here but in your source code point 3 you say “make it your own”. Doesn’t this means someone can legally claim it his or her own and therefore change the license to exclude you and the blender community access to it; say a pay-to-use-3d-editor-compitor-company who doesn’t like blender so much?