Ideal Binary Blog 

You will find information on all aspects of our work here on our company blog.

Enter your email to hear about our products.

iPhone Content Creation with Blender – Part 2

This is part 2 in a series of posts describing how to extend Blender to fit with your own content production process, specifically with regard to producing content for iPhone.

The 2.5 release of Blender is just around the corner. From the looks of the feature improvements already described on the Blender site we can expect some substantial changes to almost every aspect of the application. One significant area is the UI, in particular scripting updates to the UI.

What I'll describe here is relevant to scripting a UI for an exporter using the 2.49.2 release of Blender. Below is a snapshot of the UtopiaGL exporter in Blender.

ExporterUI

One of the things that bugged me about setting up a UI for the UtopiaGL exporter was that Blender exposes a pretty low-level API for this purpose. This means that you end up dealing with absolute coordinates when positioning textboxes and buttons and so on. From looking at some of the existing Blender exporter plugins, the following type of UI code is not uncommon.

def draw_gui():
    ... # globals removed to save space!

    # Title
    glClear(GL_COLOR_BUFFER_BIT)
    glRasterPos2d(10, 290)
    Text("UtopiaGL .model Export")

    # VNormals / UVs / VColors / VWeights
    Label( "Properties To Export", 430, 190, 120, 20 )

    g_toggle_outputvnormals = Toggle("Vertex Normals", EVENT_NOEVENT, 430, 160, 100, 20, g_toggle_outputvnormals.val, "Output Vertex Normals" )
    g_toggle_outputuvs = Toggle("Vertex UVs", EVENT_NOEVENT, 540, 160, 100, 20, g_toggle_outputuvs.val, "Output Vertex UV Coordinates" )
    g_toggle_outputvcolors = Toggle("Vertex Colors", EVENT_NOEVENT, 430, 130, 100, 20, g_toggle_outputvcolors.val, "Output Vertex Colors" )
    g_toggle_outputvweights = Toggle("Vertex Weights", EVENT_NOEVENT, 540, 130, 100, 20, g_toggle_outputvweights.val, "Output Vertex Weights" )

    Label( "Faces", 430, 90, 80, 20 )
    g_menu_facewinding = Menu("Face Winding %t|Counter-Clockwise %x1|Clockwise %x2|", EVENT_NOEVENT, 430, 60, 150, 20, g_menu_facewinding.val, "Face winding to use" )

    # Content Root / Model File

    Label( "Content Root Path", 10, 240, 80, 20 )
    g_content_root = String("", EVENT_NOEVENT, 10, 210, 300, 20, g_content_root.val, 255, "Content root path")
    Button( "Browse", EVENT_CHOOSE_CONTENT_ROOT, 310, 210, 80, 20 )

    Label( "Model File", 10, 180, 80, 20 )
    g_filename = String("", EVENT_NOEVENT, 10, 150, 300, 20, g_filename.val, 255, "Model file to save")
    Button( "Browse", EVENT_CHOOSE_FILENAME, 310, 150, 80, 20 )

    g_toggle_outputshaders = Toggle("Output Shaders/Skins", EVENT_NOEVENT, 10, 120, 130, 20, g_toggle_outputshaders.val, "Output Shader and Skin files" )

    # Log / Log Level

    Label( "Logging", 10, 90, 80, 20 )
    g_toggle_outputtolog = Toggle("Output Log", EVENT_NOEVENT, 10, 60, 80, 20, g_toggle_outputtolog.val, "Output export progress to log file" )
    Label( "Log Level", 160, 60, 80, 20 )
    g_integer_loglevel = Menu("Log Level %t|Debug %x1|Info %x2|Warning %x3|Error %x4|Critical %x5", EVENT_NOEVENT, 230, 60, 80, 20, g_integer_loglevel.val, "Logging Level to use" )

    # Export / Exit
                
    Button( "Export", EVENT_SAVE_MODEL, 10, 10, 80, 20 )
    Button( "Exit", EVENT_EXIT ,100, 10, 80, 20 )

The above approach works and if your UI is simple and not subject to change you should be fine to implement a UI like this. If on the other hand, you plan to iteratively extend the exporter as and when new requirements appear, you'll probably want something a little more dynamic. For example, inserting a button in the middle of a UI implemented like this means calculating and changing the coordinates of all other UI elements in the surrounding area.

Silverlight and WPF have various types of Panel controls that makes insertion and removal of controls in a layout very easy. I didn't have time to implement a complete panel control, so instead I implemented a Cursor object that allows me to output UI elements at the current location of the Cursor.

class Cursor:
        """A Cursor object, use to maintain context of where to insert UI elements."""

        def __init__(self):
                self.x = 10
                self.y = 10
                self.width = 80
                self.height = 20
                self.virticalpad = 10
                self.horizontalpad = 10

        def set_x(self, newx):
                self.x = newx
        
        def set_y(self, newy):
                self.y = newy
        
        def set_width(self, newwidth):
                self.width = newwidth
        
        def set_height(self, newheight):
                self.height = newheight

        def set_virtical_pad(self, newvirticalpad):
                self.virticalpad = newvirticalpad
        
        def set_horizontal_pad(self, newhorizontalpad):
                self.horizontalpad = newhorizontalpad

        def move_up(self):
                self.y = self.y + (self.height + self.virticalpad)

        def move_down(self):
                self.y = self.y - (self.height + self.virticalpad)

        def offset_up(self, up):
                self.y = self.y + up

        def offset_down(self, down):
                self.y = self.y - down

        def move_left(self):
                self.x = self.x - (self.width + self.horizontalpad)

        def move_right(self):
                self.x = self.x + (self.width + self.horizontalpad)

        def offset_left(self, left):
                self.x = self.x - left

        def offset_right(self, right):
                self.x = self.x + right

        def Button(self, title, handle):
                Button(title, handle, self.x, self.y, self.width, self.height)

        def Toggle(self, title, handle, value, tip):
                return Toggle(title, handle, self.x, self.y, self.width, self.height, value, tip)
        
        def Label(self, title):
                Label(title, self.x, self.y, self.width, self.height)
        
        def Menu(self, options, handle, value, tip):
                return Menu(options, handle, self.x, self.y, self.width, self.height, value, tip)

        def String(self, text, handle, value, l, tip):
                return String(text, handle, self.x, self.y, self.width, self.height, value, l, tip)

By using the cursor we can convert the hard-coded UI code from above to look more like the following. Note, this code now makes it much easier to add and remove UI elements without the need to update the coordinates of the surrounding elements. It does increase the amount of code a little, but the trade off is probably worth it.

def draw_gui():
        ... # globals removed to save space!

        glClear(GL_COLOR_BUFFER_BIT)

        # Export / Exit

        cursor = Cursor()
        cursor.Button( "Export", EVENT_SAVE_MODEL)
        cursor.move_right()
        cursor.Button( "Exit", EVENT_EXIT)

        # Log / Log Level

        cursor.move_left()
        cursor.move_up()
        cursor.offset_up(20)

        g_toggle_outputtolog = cursor.Toggle("Output Log", EVENT_NOEVENT, g_toggle_outputtolog.val, "Output export progress to log file")
        cursor.offset_right(150)
        cursor.Label( "Log Level" )
        cursor.move_right()
        g_integer_loglevel = cursor.Menu("Log Level %t|Debug %x1|Info %x2|Warning %x3|Error %x4|Critical %x5", EVENT_NOEVENT, g_integer_loglevel.val, "Logging Level to use")
        cursor.set_x(10)
        cursor.move_up()
        cursor.Label( "Logging")

        # Content Root / Model File

        cursor.move_up()
        cursor.set_width(130)
        g_toggle_outputshaders = cursor.Toggle("Output Shaders/Skins", EVENT_NOEVENT, g_toggle_outputshaders.val, "Output Shader and Skin files" )

        cursor.move_up()
        cursor.set_width(300)
        g_filename = cursor.String("", EVENT_NOEVENT, g_filename.val, 255, "Model file to save" )
        cursor.offset_right(300)
        cursor.set_width(80)
        cursor.Button( "Browse", EVENT_CHOOSE_FILENAME )
        cursor.move_up()
        cursor.set_x(10)
        cursor.Label( "Model File")

        cursor.move_up()
        cursor.set_width(300)
        g_content_root = cursor.String("", EVENT_NOEVENT, g_content_root.val, 255, "Content root path" )
        cursor.offset_right(300)
        cursor.set_width(80)
        cursor.Button( "Browse", EVENT_CHOOSE_CONTENT_ROOT)
        cursor.move_up()
        cursor.set_x(10)
        cursor.Label( "Content Root Path")

        # Title

        cursor.move_up()
        cursor.move_up()
        cursor.set_width(300)
        cursor.Label( "UtopiaGL .model Export")

        # VNormals / UVs / VColors / VWeights
        
        cursor.set_x(430)
        cursor.set_y(10)
        cursor.move_up()
        cursor.offset_up(20)
        g_menu_facewinding = cursor.Menu("Face Winding %t|Counter-Clockwise %x1|Clockwise %x2|", EVENT_NOEVENT, g_menu_facewinding.val, "Face winding to use" )
        
        cursor.move_up()
        cursor.Label( "Faces")

        cursor.move_up()
        cursor.offset_up(10)
        cursor.set_width(100)

        g_toggle_outputvcolors = cursor.Toggle("Vertex Colors", EVENT_NOEVENT, g_toggle_outputvcolors.val, "Output Vertex Colors")
        cursor.move_right()
        g_toggle_outputvweights = cursor.Toggle("Vertex Weights", EVENT_NOEVENT, g_toggle_outputvweights.val, "Output Vertex Weights")

        cursor.move_left()
        cursor.move_up()
        g_toggle_outputvnormals = cursor.Toggle("Vertex Normals", EVENT_NOEVENT, g_toggle_outputvnormals.val, "Output Vertex Normals")
        cursor.move_right()
        g_toggle_outputuvs = cursor.Toggle("Vertex UVs", EVENT_NOEVENT, g_toggle_outputuvs.val, "Output Vertex UV Coordinates")

        cursor.move_left()
        cursor.move_up()
        cursor.set_width(120)
        cursor.Label( "Properties To Export")

Here's hoping the improvements made in the 2.5 release make this stuff redundant. But for now, this is a manageable way to implement an exporter UI in Blender to make updates and modifications a little easier.

I didn't get around to writing about the Material to Shader mismatch that I mentioned in the previous post. So, there's definitely going to be a part 3 to this series at a minimum. Hopefully, I'll get to it soon.

iPhone Content Creation with Blender – Part 1

This is part 1 in a series of posts describing how to extend Blender to fit with your own content production process, specifically with regard to producing content for iPhone.

~ 3D Bookshelf ~

3D Bookshelf is the world's first fully 3D eBook Reader. Available now on the AppStore for iPhone & iPod touch!

Efficient content production is vital when it comes to reducing development costs. When I started reviewing our own internal processes and tools, I began to see some big opportunities to optimise our content production process, and at the same time dramatically reduce our costs.

This serves two purposes. Firstly, we save some money up front and satisfy our customers quicker. Second, clients will be more likely to license our system if they know they can also save on content production costs.

The first step towards this goal was to make use of Blender in our content production process.

Blender

Blender is an advanced open source 3D content creation application. It's been around for quite some time, but I've only recently given it a full evaluation. The primary benefits it has to offer over similar commercial packages like 3DS Max and Maya are:

  1. It's free. The commercial alternatives can cost anything between 1000 US dollars to about 3500 US dollars.
  2. It's really easy to use. It makes excellent use of keyboard short-cuts to allow you develop efficient processes.
  3. It's really easy to extend. For example, writing an exporter means creating one python file and adding it to a scripts folder. No C/C++ projects or solutions, no need for Visual Studio etc.
  4. It's feature rich. For example, apart from the standard modeling capabilities, it's integrated with Bullet physics. You can model scenes with water-flows and so on.
  5. It's cross platform. Linux, Mac OSX and Windows.

Extending with Python

The most significant component of our iPhone middleware engine (UtopiaGL) is the rendering engine. It uses a proprietary file format which ensures that the 3D content is both legal and also optimised for the engine. In order to load content produced with Blender into the rendering engine, it must first be exported to our proprietary format.

Writing exporters for Blender is extremely straightforward, so long as you know Python. I hadn't used Python for a long time so I had to do a quick refresher course. Luckily, the tutorial page on the Python site is about all you need to get up and running.

Once I had the shell of my exporter set up, I decide to add full logging support. Python has a feature rich logging system so this was very easy to add. I wrote a simple Log class to abstract away the underlying logging system so I could turn the logging completely off, if required. I also added the logging initialisation and shutdown code to this class.

class Log:
        """My log class."""

        def initialize_logger(self, filepath, outputlog, loglevelindex):
                """Create the logger for the exporter."""
                self.outputlog = outputlog
                if self.outputlog:
                        logfilepath = filepath + '.log'
                        self.logger = logging.getLogger('BlenderUtopiaGLExporter')
                        loglevels = [logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR, logging.CRITICAL]
                        self.logger.setLevel(loglevels[loglevelindex])
                        self.logfilehandler = logging.FileHandler(logfilepath, 'w')
                        self.logger.addHandler(self.logfilehandler)
                        self.logger.info('Begin')
                        self.logger.info('Exporting to file: <%s>' % filepath)

        def close_logger(self):
                """Close the logfilehandler to ensure the log file is closed."""
                if self.outputlog:
                        self.logger.info('End')
                        self.logger.removeHandler(self.logfilehandler)
                        self.logfilehandler.flush()
                        self.logfilehandler.close()

        def info(self, text):
                if self.outputlog:
                        self.logger.info(text)
   
        def debug(self, text):
                if self.outputlog:
                        self.logger.debug(text)
   
        def warning(self, text):
                if self.outputlog:
                        self.logger.warning(text)
   
        def error(self, text):
                if self.outputlog:
                        self.logger.error(text)
   
        def critical(self, text):
                if self.outputlog:
                        self.logger.critical(text)

        def exception(self, text):
                if self.outputlog:
                        self.logger.exception(text)

Having good logging support can make things easier if you want to be able to support the exporter beyond a small development team.  I was a little surprised not to see this approach being used by any of the exporters supplied with Blender. Most seem to opt for just outputting to the console.

Debugging the Exporter

Logging support will help identify the cause of issues reported by the exporter when it's used in the wild. But you'll want to be able to use the python debugger to track down problems during development and to investigate the root cause of problems identified in the logs. Unfortunately, this isn't as straightforward as it should be, but it is possible.

First, you need to get hold of Winpdb. This is a platform independent python debugger. You can find out more about setting this up at the Winpdb site. On windows, don't forget to add the PYTHONHOME environment variable to your system before you install. Mine is set to point to C:\Python26.

Once you have the python debugger set up, you must load your exporter script into the text editor built into Blender and run with ALT-P (press this while focus is in the text editor window). Add the following line to your script beforehand. This is what kicks off the embedded debugging session.

# To use embedded debugging and specify a hardcoded debug
# session password, use the following..
import rpdb2; rpdb2.start_embedded_debugger("password")

# Or, to use embedded debugging with an interactive password,
# use this instead of the above..
# import rpdb2; rpdb2.start_embedded_debugger_interactive_password()

Once the script has been run using the approach above, you'll be able to attach Winpdb to this session. Run %PYTHONHOME%\Scripts\winpdb and from the File menu select Attach. Enter "password" or what ever you've specified as the password for your debugging session and you'll be up and running in the debugger.

What's Next?

With a logging system in place and the ability to debug issues as they arise, it's possible to progress quickly through the development of python scripts from within Blender.

In Part 2 of this series I'll talk about how to add a UI to a Blender exporter. I'll also talk about the Material to Shader mismatch that must be dealt with when exporting 3D content from Blender to be used by OpenGL.